From f41994b11ea8721463cc40f5a6b4c0c6860e3bcb Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 8 Oct 2026 16:37:56 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9A=A1=20Lower=20QC=20to=20QCO=20in=20so?= =?UTF-8?q?urce=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thread quantum state through ordinary rewrite patterns in an explicit source-order traversal. Retain source references until their users are lowered, preserving region ownership and function argument order. Keep folding and CSE during normalization without generic region cleanup. Cover interleaved calls, nested unitary calls, and folded register aliases. Leave QCO-to-QC and the base branch's verification policy unchanged. Assisted-by: OpenAI Codex --- .../qc-to-qco-on-boundary-verification.md | 91 +++ mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 748 ++++++++---------- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 110 +++ 3 files changed, 517 insertions(+), 432 deletions(-) create mode 100644 .agent/plans/qc-to-qco-on-boundary-verification.md diff --git a/.agent/plans/qc-to-qco-on-boundary-verification.md b/.agent/plans/qc-to-qco-on-boundary-verification.md new file mode 100644 index 0000000000..5896cd05b8 --- /dev/null +++ b/.agent/plans/qc-to-qco-on-boundary-verification.md @@ -0,0 +1,91 @@ +# Source-ordered QC → QCO on boundary verification + +Status: complete. Source-ordered conversion and targeted normalization are +implemented and locally validated against the pinned base. End-to-end performance +is mixed; whole-file C++ lint and human review remain merge gates. + +## Scope and decisions + +The implementation changes only `mlir/lib/Conversion/QCToQCO/QCToQCO.cpp` +and `mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp`. +Use ordinary rewrite patterns in explicit source order. Keep source reference +identities alive until their users have been lowered; move nested regions +without destroying their original children. Preserve type conversion, static +normalization, supported input checks, and linear output validation. + +Reusing preflight state only when normalization is skipped did not improve the +selected workloads: OpenQASM register inputs always require normalization. +That experiment is not retained. Normalization can invalidate source identities, +so eliminating post-normalization collection needs a stronger invariant. + +Sampling the factor247 conversion identified region liveness cleanup inside the +greedy normalizer as a substantial cost. Use MLIR's existing +`GreedySimplifyRegionLevel::Disabled` for normalization; this pass needs index +folding and static-reference normalization, not control-flow optimization. + +The baseline is PR #2698 at `2cf5d436150a6fe953fbc6d8f0c44f126bba3c1f`. +Keep its QCO → QC lowering, boundary-verification policy, mapping, and all other +pipelines unchanged. Existing ordered reverse-conversion work is outside scope. + +## Validation + +Both variants use the Release preset, standalone native builds, and the same +assertion-free LLVM/MLIR 23.1.0 toolchain. Run the QCToQCO, QCOToQC, and Compiler +GoogleTest targets under `build/release/mlir/unittests/` to reproduce the native +checks. All 602 baseline tests and 605 final candidate tests pass. The three +added tests cover interleaved call signatures with owned results, unitary calls +inside nested regions, and register aliasing exposed by index folding. + +AddressSanitizer with assertions passes all 174 forward-conversion tests. The +converter and its test translation unit are instrumented; dependencies are not. +Leak detection is disabled. Repository lint (`uvx nox -s lint`) and +`git diff --check` pass. + +The performance comparison uses frozen Benchpress inputs from revision +`045e7ca6a8e5302826d82f07d42b5689ca61db3c`: QFT with 18 and 160 qubits, QRAM with +20 qubits, Ising with 26 qubits, cat with 260 qubits, W-state with 380 qubits, +and factor247 with 15 qubits. Seven unique circuits cover forward conversion; +seven circuit/topology pairs cover full compilation, excluding QFT-160 and +including QFT-18 on both all-to-all and linear connectivity. + +The final direct comparison includes both changes against unmodified PR #2698, +not a product of earlier incremental ratios. Each pair has three serial rounds +with alternating variant order and seed 42. All 84 samples pass: 42 per variant, +without failures, errors, skips, timeouts, or interrupted samples. Source, input, +configuration, binary, and output hashes are checked. Every paired output IR +and returned gate count is identical, including repetitions. + +Forward conversion has seven complete pairs: geometric candidate/baseline ratio +0.616, with summed case means of 11.393 s before and 6.460 s after. Factor247 +improves from 10.520 s to 5.968 s; QFT-160 improves from 0.827 s to 0.461 s. + +Full compilation also has seven complete pairs: geometric ratio 0.986, but +summed case means increase from 65.070 s to 70.430 s. QFT-18/linear regresses +13.5% and factor247/square regresses 8.3%. Factor247's mean target-compilation +phase rises from 47.344 s to 56.035 s while its forward conversion falls from +10.561 s to 6.608 s. The cause of the end-to-end regression is not established. +Keep all samples and do not claim a universal full-compilation speedup. + +A fresh isolated wheel passes 977 focused Python tests with no skips: +`test_mlir.py`, `test_mlir_loops.py`, `test_mlir_qiskit_translation.py`, +`test_mlir_qiskit_target.py`, `test_mlir_parameter_binding.py`, and +`test_mlir_integer_interchange.py`. Run them with `python -m pytest` and +`-o addopts=` in the wheel's environment. + +Forward-conversion timing excludes the input copy. Full timing includes the QC +copy, QC → QCO, target compilation, and QCO → QC. QASM loading, target setup, +extra output verification, inspection, and serialization are outside timers. +The native targets use frozen FlexibleBackend connectivity and the common +`sx`, `x`, `rz`, `cz`, measurement, and reset basis without calibration metadata. + +## Limits + +This is a native API comparison on selected Benchpress inputs, not a run of the +pytest-benchmark suite. Each fresh process performs one timed call, with no +warmup or adaptive calibration and a 600-second whole-process deadline. Three +fixed-seed rounds on a shared desktop do not establish statistical significance +for small differences. Two-qubit depth is not independently measured. +Whole-changed-file C++ lint was attempted against the pinned revision but +stopped before checking any files because clang-tidy 23 is unavailable locally. +The supported conversion subset is unchanged; this work does not add support +for unstructured control flow. diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 29be02835a..295ad164bf 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -10,7 +10,6 @@ #include "mqt/Conversion/QCToQCO/QCToQCO.h" -#include "mqt/Conversion/ConversionUtils.h" #include "mqt/Dialect/CBit/IR/CBitDialect.h" #include "mqt/Dialect/CBit/IR/CBitOps.h" #include "mqt/Dialect/MQT/IR/MQTDialect.h" @@ -38,6 +37,7 @@ #include "mlir/IR/Types.h" #include "mlir/IR/Value.h" #include "mlir/IR/ValueRange.h" +#include "mlir/Rewrite/PatternApplicator.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" #include "mlir/Support/WalkResult.h" @@ -126,10 +126,6 @@ struct LoweringState { /// Local physical tensors released at function return, in allocation order. DenseMap> localReferenceRegisters; - /// Transient canonical keys for modifier block arguments replaced by - /// dialect-conversion signature conversion. - DenseMap convertedQubitAliases; - /// Map from an operation to its used QC qubits inside its regions DenseMap> regionQubitMap; @@ -165,12 +161,12 @@ struct LoweringState { /// Share qubit and register mappings across conversion patterns. template -class StatefulOpConversionPattern : public OpConversionPattern { +class LoweringPattern : public OpRewritePattern { public: - StatefulOpConversionPattern(TypeConverter& typeConverter, - MLIRContext* context, LoweringState* state) - : OpConversionPattern(context), state_(state), + LoweringPattern(TypeConverter& typeConverter, MLIRContext* context, + LoweringState* state) + : OpRewritePattern(context), state_(state), typeConverter_(&typeConverter) {} [[nodiscard]] LoweringState& getState() const { return *state_; } @@ -222,21 +218,9 @@ findRegionLocalMap(DenseMap& map, Operation* anchor, return {nullptr, nullptr}; } -/// Canonicalizes a source qubit key after block signature conversion. -[[nodiscard]] static Value canonicalQubitKey(const LoweringState& state, - Value qubit) { - for (auto alias = state.convertedQubitAliases.find(qubit); - alias != state.convertedQubitAliases.end(); - alias = state.convertedQubitAliases.find(qubit)) { - qubit = alias->second; - } - return qubit; -} - /// Resolves the latest QCO SSA value for a QC qubit reference. [[nodiscard]] static Value lookupMappedQubit(LoweringState& state, Operation* anchor, Value qcQubit) { - qcQubit = canonicalQubitKey(state, qcQubit); const auto& [qubitMap, qubitValue] = findRegionLocalMap(state.qubitMap, anchor, qcQubit); assert(qubitMap != nullptr && qubitValue != nullptr && "QC qubit not found"); @@ -257,7 +241,6 @@ findRegionLocalMap(DenseMap& map, Operation* anchor, /// Updates the latest QCO SSA value for a QC qubit reference. static void assignMappedQubit(LoweringState& state, Operation* anchor, Value qcQubit, Value qcoQubit) { - qcQubit = canonicalQubitKey(state, qcQubit); auto [qubitMap, qubitValue] = findRegionLocalMap(state.qubitMap, anchor, qcQubit); if (qubitValue != nullptr) { @@ -342,7 +325,7 @@ static void seedRegionMappings(LoweringState& state, Region& region, ValueRange qcoQubits, ValueRange tensors) { auto& qubitMap = state.qubitMap[®ion]; for (auto [qcQubit, qcoQubit] : llvm::zip_equal(qcQubits, qcoQubits)) { - qubitMap[canonicalQubitKey(state, qcQubit)] = qcoQubit; + qubitMap[qcQubit] = qcoQubit; } auto& tensorMap = state.tensorMap[®ion]; for (auto [reg, tensor] : llvm::zip_equal(registers, tensors)) { @@ -450,7 +433,10 @@ static void commitQubits(LoweringState& state, Operation* anchor, [[nodiscard]] static LogicalResult normalizeStaticQubits(ModuleOp moduleOp) { RewritePatternSet patterns(moduleOp.getContext()); qc::StaticOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); - if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + // Normalization needs folding and static-qubit hoisting, not CFG cleanup. + GreedyRewriteConfig config; + config.setRegionSimplificationLevel(GreedySimplifyRegionLevel::Disabled); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns), config))) { return failure(); } IRRewriter rewriter(moduleOp.getContext()); @@ -834,20 +820,25 @@ static void collectStructuredCaptures(Operation* root, LoweringState& state) { }); } -/// Seeds region-owned modifier state after signature conversion. -static void initializeModifierRegionState(Operation* modifier, - ValueRange sourceArguments, - LoweringState& state) { - auto& region = modifier->getRegion(0); - const auto convertedArguments = region.front().getArguments(); - for (auto [source, converted] : - llvm::zip_equal(sourceArguments, convertedArguments)) { - state.convertedQubitAliases[source] = converted; +// Keep the block and its argument identities; only their types change. All +// uses inside the moved body are lowered before the pass verifier runs. +static void moveModifierRegion(Region& source, Region& destination, + PatternRewriter& rewriter) { + rewriter.inlineRegionBefore(source, destination, destination.end()); + // Modifier arguments are qubits by contract. Preserve their identities so + // the original child operations remain in the traversal worklist. + for (auto argument : destination.front().getArguments()) { + argument.setType(qco::QubitType::get(rewriter.getContext())); } +} + +static void initializeModifierRegionState(Operation* op, LoweringState& state) { + auto& region = op->getRegion(0); + auto args = region.front().getArguments(); + // No old->new alias is needed: old and new values are identical. state.modifierRegionQubits[®ion] = - SmallVector(convertedArguments.begin(), convertedArguments.end()); - seedRegionMappings(state, region, convertedArguments, {}, convertedArguments, - {}); + SmallVector(args.begin(), args.end()); + seedRegionMappings(state, region, args, {}, args, {}); } namespace { @@ -859,12 +850,11 @@ namespace { /// consumed exactly once. For allocations (including static qubits), the sink /// is `qco.sink`. This pattern inserts `qco.sink` operations for all still-live /// qubits tracked in the lowering state right before the return. -struct ConvertFuncReturnOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertFuncReturnOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(func::ReturnOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(func::ReturnOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* funcRegion = op->getParentRegion(); auto& map = state.qubitMap[funcRegion]; @@ -875,14 +865,13 @@ struct ConvertFuncReturnOp final : StatefulOpConversionPattern { SmallVector returnValues; returnValues.reserve(op.getNumOperands()); DenseSet liveQubits; - for (auto [qcOperand, adaptorOperand] : - llvm::zip_equal(op.getOperands(), adaptor.getOperands())) { + for (auto qcOperand : op.getOperands()) { if (auto* it = map.find(qcOperand); it != map.end() && it->second) { auto latest = it->second; returnValues.emplace_back(latest); liveQubits.insert(latest); } else { - returnValues.emplace_back(adaptorOperand); + returnValues.emplace_back(qcOperand); } } auto function = op->getParentOfType(); @@ -947,21 +936,15 @@ class QCToQCOTypeConverter final : public TypeConverter { } }; -struct ConvertFuncOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertFuncOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(func::FuncOp op, OpAdaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(func::FuncOp op, + PatternRewriter& rewriter) const override { if (getQCToQCOTypeConverter()->isSignatureLegal(op.getFunctionType())) { return failure(); } - TypeConverter::SignatureConversion signature(op.getNumArguments()); - if (failed(getQCToQCOTypeConverter()->convertSignatureArgs( - op.getArgumentTypes(), signature))) { - return failure(); - } SmallVector inputs; SmallVector results; if (failed(getQCToQCOTypeConverter()->convertTypes(op.getArgumentTypes(), @@ -993,15 +976,20 @@ struct ConvertFuncOp final : StatefulOpConversionPattern { if (op.isExternal()) { return success(); } - auto convertedEntry = rewriter.convertRegionTypes( - &op.getBody(), *getQCToQCOTypeConverter(), &signature); - if (failed(convertedEntry)) { - return failure(); + // Retain source arguments until their QC users are lowered. Inserting + // each converted argument next to its source preserves interleaved order + // when the old arguments are erased at the end of the pass. + DenseMap convertedArguments; + auto& block = op.getBody().front(); + for (unsigned index : llvm::reverse(qubitArguments)) { + auto converted = block.insertArgument(index + 1, inputs[index], + originalArguments[index].getLoc()); + convertedArguments[originalArguments[index]] = converted; } auto& map = getState().qubitMap[&op.getBody()]; auto& functionArguments = getState().functionQubitArguments[op]; for (unsigned index : qubitArguments) { - Value converted = (*convertedEntry)->getArgument(index); + Value converted = convertedArguments.lookup(originalArguments[index]); if (auto reg = getState().registerIds.find(originalArguments[index]); reg != getState().registerIds.end()) { const auto id = reg->second; @@ -1016,12 +1004,11 @@ struct ConvertFuncOp final : StatefulOpConversionPattern { } }; -struct ConvertFuncCallOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertFuncCallOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(func::CallOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(func::CallOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); SmallVector qcQubits; for (auto operand : op.getOperands()) { @@ -1035,7 +1022,7 @@ struct ConvertFuncCallOp final : StatefulOpConversionPattern { return failure(); } auto materialized = materializeQubits(state, op, qcQubits, rewriter); - SmallVector operands(adaptor.getOperands()); + SmallVector operands(op.getOperands()); SmallVector quantumArguments; size_t qubitIndex = 0; for (auto [index, source] : llvm::enumerate(op.getOperands())) { @@ -1077,17 +1064,24 @@ struct ConvertFuncCallOp final : StatefulOpConversionPattern { } } commitQubits(state, op, qcQubits, qubitResults, materialized, rewriter); - rewriter.replaceOp(op, call.getResults().take_front(op.getNumResults())); + // Classical values use normal SSA replacement; QC references keep their + // identity and resolve through the state map until deferred source erasure. + for (auto [source, result] : + llvm::zip_equal(op.getResults(), + call.getResults().take_front(op.getNumResults()))) { + if (!isa(source.getType())) { + rewriter.replaceAllUsesWith(source, result); + } + } return success(); } }; -struct ConvertQCCallOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCCallOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::CallOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::CallOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); const auto firstQubit = llvm::find_if(op.getOperands(), [](Value operand) { return isa(operand.getType()); @@ -1096,7 +1090,7 @@ struct ConvertQCCallOp final : StatefulOpConversionPattern { std::distance(op.getOperands().begin(), firstQubit)); auto qcQubits = op.getOperands().drop_front(numParams); auto materialized = materializeQubits(state, op, qcQubits, rewriter); - SmallVector operands(adaptor.getOperands().take_front(numParams)); + SmallVector operands(op.getOperands().take_front(numParams)); llvm::append_range(operands, materialized.values); auto call = qco::CallOp::create(rewriter, op.getLoc(), op.getCalleeAttr(), operands); @@ -1119,13 +1113,11 @@ struct ConvertQCCallOp final : StatefulOpConversionPattern { /// ```mlir /// %tensor = qtensor.alloc(%c3) : tensor<3x!qco.qubit> /// ``` -struct ConvertMemRefAllocOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertMemRefAllocOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(memref::AllocOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(memref::AllocOp op, + PatternRewriter& rewriter) const override { if (!isa(op.getType().getElementType())) { return failure(); } @@ -1142,7 +1134,7 @@ struct ConvertMemRefAllocOp final qtensor::AllocOp alloc; if (shape[0] == ShapedType::kDynamic) { alloc = qtensor::AllocOp::create(rewriter, op.getLoc(), - adaptor.getDynamicSizes()[0]); + op.getDynamicSizes()[0]); } else { auto size = arith::ConstantIndexOp::create(rewriter, op.getLoc(), shape[0]); @@ -1154,20 +1146,18 @@ struct ConvertMemRefAllocOp final auto memref = op.getResult(); assignMappedTensor(state, alloc, lookupRegisterId(state, memref), alloc.getResult()); - rewriter.replaceOp(op, alloc.getResult()); + // Source identity retained until all QC users are lowered. return success(); } }; /// Rebuild an immutable buffer of physical references as a qubit tensor. -struct ConvertMemRefAllocaOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertMemRefAllocaOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(memref::AllocaOp op, OpAdaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(memref::AllocaOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); const auto buffer = state.referenceBuffers.find(op.getResult()); if (buffer == state.referenceBuffers.end()) { @@ -1190,7 +1180,7 @@ struct ConvertMemRefAllocaOp final const auto reg = lookupRegisterId(state, op.getResult()); assignMappedTensor(state, op, reg, tensor.getResult()); state.localReferenceRegisters[op->getParentRegion()].push_back(reg); - rewriter.replaceOp(op, tensor.getResult()); + // Source identity retained until all QC users are lowered. return success(); } }; @@ -1202,12 +1192,11 @@ struct ConvertMemRefAllocaOp final /// %q = memref.load %memref[%c0] : memref<3x!qc.qubit> /// ``` /// Quantum operations extract and reinsert the referenced qubit locally. -struct ConvertMemRefLoadOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertMemRefLoadOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(memref::LoadOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(memref::LoadOp op, + PatternRewriter& rewriter) const override { if (!isa(op.getMemRefType().getElementType())) { return failure(); } @@ -1216,21 +1205,20 @@ struct ConvertMemRefLoadOp final : StatefulOpConversionPattern { const auto accessIt = state.registerAccesses.find(op.getResult()); assert(accessIt != state.registerAccesses.end() && "qubit load provenance not found"); - accessIt->second.index = adaptor.getIndices().front(); + accessIt->second.index = op.getIndices().front(); - rewriter.eraseOp(op); + // Source reference retained until all QC users are lowered. return success(); } }; /// A valid register store preserves the reference already in that slot. -struct ConvertMemRefStoreOp final : OpConversionPattern { - using OpConversionPattern::OpConversionPattern; +struct ConvertMemRefStoreOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(memref::StoreOp op, OpAdaptor, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(memref::StoreOp op, + PatternRewriter& rewriter) const override { if (!isa(op.getValue().getType())) { return failure(); } @@ -1249,13 +1237,11 @@ struct ConvertMemRefStoreOp final : OpConversionPattern { /// ```mlir /// qtensor.dealloc %tensor : tensor<3x!qco.qubit> /// ``` -struct ConvertMemRefDeallocOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertMemRefDeallocOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(memref::DeallocOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(memref::DeallocOp op, + PatternRewriter& rewriter) const override { auto memref = op.getMemref(); if (!isa(memref.getType().getElementType())) { return failure(); @@ -1272,45 +1258,12 @@ struct ConvertMemRefDeallocOp final } }; -/// Converts qc.alloc to qco.alloc -/// -/// @par Example: -/// ```mlir -/// %q = qc.alloc : !qc.qubit -/// ``` -/// is converted to -/// ```mlir -/// %q = qco.alloc : !qco.qubit -/// ``` -struct ConvertQCAllocOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; - - LogicalResult - matchAndRewrite(qc::AllocOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { - auto& state = getState(); - if (failed(state.ensureAllocationMode(AllocationMode::Dynamic, - op.getOperation()))) { - return failure(); - } - auto qcQubit = op.getResult(); - - auto qcoOp = rewriter.replaceOpWithNewOp(op); - - auto qcoQubit = qcoOp.getResult(); - assignMappedQubit(state, qcoOp, qcQubit, qcoQubit); - - return success(); - } -}; - /// Sink the latest mapped qubit value and mark its QC reference as consumed. -struct ConvertQCDeallocOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCDeallocOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(DeallocOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(DeallocOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto& qubitMap = state.qubitMap[op->getParentRegion()]; auto* operation = op.getOperation(); @@ -1327,39 +1280,13 @@ struct ConvertQCDeallocOp final : StatefulOpConversionPattern { } }; -/// Preserve the static qubit index and record its initial QCO value. -struct ConvertQCStaticOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; - - LogicalResult - matchAndRewrite(qc::StaticOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { - auto& state = getState(); - if (failed(state.ensureAllocationMode(AllocationMode::Static, - op.getOperation()))) { - return failure(); - } - if (state.registerAccesses.contains(op.getQubit())) { - rewriter.eraseOp(op); - return success(); - } - auto qcQubit = op.getQubit(); - - auto qcoOp = rewriter.replaceOpWithNewOp(op, op.getIndex()); - assignMappedQubit(state, qcoOp, qcQubit, qcoOp.getQubit()); - - return success(); - } -}; - /// Measure the latest mapped qubit, retain its output in the lowering state, /// and replace the QC measurement's classical result. -struct ConvertQCMeasureOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCMeasureOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::MeasureOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::MeasureOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcQubit = op.getQubit(); @@ -1380,12 +1307,11 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { }; /// Reset the latest mapped qubit and retain its output in the lowering state. -struct ConvertQCResetOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCResetOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::ResetOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::ResetOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcQubit = op.getQubit(); @@ -1406,12 +1332,11 @@ struct ConvertQCResetOp final : StatefulOpConversionPattern { template -struct ConvertQCGateToQCO final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCGateToQCO final : LoweringPattern { + using LoweringPattern::LoweringPattern; template - auto createGate(ConversionPatternRewriter& rewriter, QCOpType op, - ValueRange qcoTargets, + auto createGate(PatternRewriter& rewriter, QCOpType op, ValueRange qcoTargets, std::index_sequence /*targets*/, std::index_sequence /*params*/) const { auto params = op.getParameters(); @@ -1420,9 +1345,8 @@ struct ConvertQCGateToQCO final : StatefulOpConversionPattern { params[ParamIndices]...); } - LogicalResult - matchAndRewrite(QCOpType op, QCOpType::Adaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(QCOpType op, + PatternRewriter& rewriter) const override { auto& state = this->getState(); auto qcTargets = op.getTargets(); auto materialized = materializeQubits(state, op, qcTargets, rewriter); @@ -1440,12 +1364,11 @@ struct ConvertQCGateToQCO final : StatefulOpConversionPattern { }; /// Converts a variadic dense qc.unitary to its value-semantics form. -struct ConvertQCUnitaryOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCUnitaryOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::UnitaryOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::UnitaryOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcQubits = op.getQubits(); @@ -1471,12 +1394,11 @@ struct ConvertQCUnitaryOp final : StatefulOpConversionPattern { /// %q_out:2 = qco.barrier %q0_in, %q1_in : !qco.qubit, !qco.qubit -> /// !qco.qubit, !qco.qubit /// ``` -struct ConvertQCBarrierOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCBarrierOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::BarrierOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::BarrierOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcQubits = op.getQubits(); @@ -1508,12 +1430,11 @@ struct ConvertQCBarrierOp final : StatefulOpConversionPattern { /// qco.yield %a_res : !qco.qubit /// } : ({!qco.qubit}, {!qco.qubit}) /// ``` -struct ConvertQCCtrlOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCCtrlOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::CtrlOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::CtrlOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcControls = op.getControls(); @@ -1532,16 +1453,10 @@ struct ConvertQCCtrlOp final : StatefulOpConversionPattern { llvm::append_range(qcoQubits, qcoOp.getTargetsOut()); commitQubits(state, operation, qcQubits, qcoQubits, materialized, rewriter); - const SmallVector sourceArguments( - op.getRegion().front().getArguments()); - // Inline region and convert the block signature to QCO types. - if (failed(moveRegion(op.getRegion(), qcoOp.getRegion(), rewriter, - getQCToQCOTypeConverter()))) { - return failure(); - } + moveModifierRegion(op.getRegion(), qcoOp.getRegion(), rewriter); - initializeModifierRegionState(qcoOp, sourceArguments, state); + initializeModifierRegionState(qcoOp, state); rewriter.eraseOp(op); return success(); @@ -1563,12 +1478,11 @@ struct ConvertQCCtrlOp final : StatefulOpConversionPattern { /// qco.yield %a0_res : !qco.qubit /// } : {!qco.qubit} -> {!qco.qubit} /// ``` -struct ConvertQCInvOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCInvOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::InvOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::InvOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcTargets = op.getTargets(); @@ -1580,16 +1494,10 @@ struct ConvertQCInvOp final : StatefulOpConversionPattern { commitQubits(state, operation, qcTargets, qcoOp.getOutputTargets(), materialized, rewriter); - const SmallVector sourceArguments( - op.getRegion().front().getArguments()); - // Inline region and convert the block signature to QCO types. - if (failed(moveRegion(op.getRegion(), qcoOp.getRegion(), rewriter, - getQCToQCOTypeConverter()))) { - return failure(); - } + moveModifierRegion(op.getRegion(), qcoOp.getRegion(), rewriter); - initializeModifierRegionState(qcoOp, sourceArguments, state); + initializeModifierRegionState(qcoOp, state); rewriter.eraseOp(op); return success(); @@ -1611,12 +1519,11 @@ struct ConvertQCInvOp final : StatefulOpConversionPattern { /// qco.yield %a0_res /// } : {!qco.qubit} -> {!qco.qubit} /// ``` -struct ConvertQCPowOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCPowOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::PowOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::PowOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto qcTargets = op.getTargets(); @@ -1629,16 +1536,10 @@ struct ConvertQCPowOp final : StatefulOpConversionPattern { commitQubits(state, operation, qcTargets, qcoOp.getQubitsOut(), materialized, rewriter); - const SmallVector sourceArguments( - op.getRegion().front().getArguments()); - // Inline region and convert the block signature to QCO types. - if (failed(moveRegion(op.getRegion(), qcoOp.getRegion(), rewriter, - getQCToQCOTypeConverter()))) { - return failure(); - } + moveModifierRegion(op.getRegion(), qcoOp.getRegion(), rewriter); - initializeModifierRegionState(qcoOp, sourceArguments, state); + initializeModifierRegionState(qcoOp, state); rewriter.eraseOp(op); return success(); @@ -1655,12 +1556,11 @@ struct ConvertQCPowOp final : StatefulOpConversionPattern { /// ```mlir /// qco.yield %targets : !qco.qubit /// ``` -struct ConvertQCYieldOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertQCYieldOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(qc::YieldOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(qc::YieldOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto* region = op->getParentRegion(); @@ -1697,12 +1597,11 @@ struct ConvertQCYieldOp final : StatefulOpConversionPattern { /// scf.yield %t1 : tensor<3x!qco.qubit> /// } /// ``` -struct ConvertSCFForOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertSCFForOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(scf::ForOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(scf::ForOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto& registerMap = state.regionRegisterMap[op]; @@ -1780,12 +1679,11 @@ struct ConvertSCFForOp final : StatefulOpConversionPattern { /// scf.yield %q2 : !qco.qubit /// } /// ``` -struct ConvertSCFWhileOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertSCFWhileOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(scf::WhileOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(scf::WhileOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto& registerMap = state.regionRegisterMap[op]; @@ -1884,12 +1782,11 @@ struct ConvertSCFWhileOp final : StatefulOpConversionPattern { /// qco.yield %arg0 : !qco.qubit /// } /// ``` -struct ConvertSCFIfOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertSCFIfOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(scf::IfOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(scf::IfOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto& registerMap = state.regionRegisterMap[op]; @@ -1976,13 +1873,11 @@ struct ConvertSCFIfOp final : StatefulOpConversionPattern { /// qco.yield %q2 : !qco.qubit /// } /// ``` -struct ConvertSCFIndexSwitchOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertSCFIndexSwitchOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(scf::IndexSwitchOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(scf::IndexSwitchOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); auto& registerMap = state.regionRegisterMap[op]; @@ -2044,12 +1939,11 @@ struct ConvertSCFIndexSwitchOp final /// ```mlir /// scf.yield %targets /// ``` -struct ConvertSCFYieldOp final : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertSCFYieldOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(scf::YieldOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(scf::YieldOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); @@ -2077,13 +1971,11 @@ struct ConvertSCFYieldOp final : StatefulOpConversionPattern { /// ```mlir /// scf.condition(%cond) %targets /// ``` -struct ConvertSCFConditionOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +struct ConvertSCFConditionOp final : LoweringPattern { + using LoweringPattern::LoweringPattern; - LogicalResult - matchAndRewrite(scf::ConditionOp op, OpAdaptor /*adaptor*/, - ConversionPatternRewriter& rewriter) const override { + LogicalResult matchAndRewrite(scf::ConditionOp op, + PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); @@ -2096,180 +1988,172 @@ struct ConvertSCFConditionOp final } }; -/// Convert QC references to QCO values, threading quantum state through -/// functions and structured control flow. -/// -/// Lower terminators after their regions so they yield the final mapped values -/// independently of rewrite order. -struct QCToQCO final : impl::QCToQCOBase { - using QCToQCOBase::QCToQCOBase; - -protected: - void runOnOperation() override { - MLIRContext* context = &getContext(); - auto moduleOp = getOperation(); - - LoweringState preflightState; - if (failed(validateSupportedInput(moduleOp)) || - failed(collectRegisterAccesses(moduleOp, preflightState))) { - signalPassFailure(); - return; - } - - // Register indices still need normalization for alias validation. - const bool needsNormalization = !preflightState.registerIds.empty() || - !staticsAlreadyNormalized(moduleOp); - if (needsNormalization && failed(normalizeStaticQubits(moduleOp))) { - signalPassFailure(); - return; - } - - LoweringState normalizedState; - auto& state = needsNormalization ? normalizedState : preflightState; - - ConversionTarget target(*context); - RewritePatternSet patterns(context); - QCToQCOTypeConverter typeConverter(context); +} // namespace - if (needsNormalization && - failed(collectRegisterAccesses(moduleOp, state))) { - signalPassFailure(); - return; +/// Convert QC references to QCO values in source order, threading quantum state +/// through functions and structured control flow. +/// +/// Lower terminators after preceding operations so they yield the final mapped values. +static LogicalResult lowerToQCO(ModuleOp moduleOp) { + LoweringState preflight; + if (failed(validateSupportedInput(moduleOp)) || + failed(collectRegisterAccesses(moduleOp, preflight))) { + return failure(); + } + // Register indices still need normalization for alias validation. + const bool needsNormalization = !preflight.registerIds.empty() || + !staticsAlreadyNormalized(moduleOp); + if (needsNormalization && failed(normalizeStaticQubits(moduleOp))) { + return failure(); + } + LoweringState normalizedState; + auto& state = needsNormalization ? normalizedState : preflight; + if (needsNormalization && failed(collectRegisterAccesses(moduleOp, state))) { + return failure(); + } + SmallVector unitaryFunctions; + for (auto function : moduleOp.getOps()) { + if (mqt::isUnitaryFunction(function)) { + unitaryFunctions.push_back(function); + function->removeAttr(mqt::MQTDialect::UnitaryAttrHelper::getNameStr()); } - - SmallVector unitaryFunctions; - for (auto function : moduleOp.getOps()) { - if (mqt::isUnitaryFunction(function)) { - unitaryFunctions.emplace_back(function); - function->removeAttr(mqt::MQTDialect::UnitaryAttrHelper::getNameStr()); - } + } + llvm::scope_exit unitaryGuard([&] { + for (auto function : unitaryFunctions) { + mqt::setUnitaryFunction(function); } - auto unitaryGuard = llvm::scope_exit([&] { - for (auto function : unitaryFunctions) { - mqt::setUnitaryFunction(function); - } - }); - - // Get the quantum values captured by structured control-flow regions. - collectStructuredCaptures(moduleOp, state); - - // Configure conversion target - target.addIllegalDialect(); - target.addLegalDialect(); - - target.addDynamicallyLegalDialect([](Operation* op) { - return llvm::none_of(op->getOperandTypes(), isQubitMemrefType) && - llvm::none_of(op->getResultTypes(), isQubitMemrefType); - }); - - target.addDynamicallyLegalDialect([&](Operation* op) { - auto& regionQubitMap = state.regionQubitMap[op]; - auto& regionTensorMap = state.regionRegisterMap[op]; - return regionQubitMap.empty() && regionTensorMap.empty(); - }); - // Structured terminators are lowered in a second conversion phase. The - // first phase must finish converting every operation in a region so the - // region-local maps contain the final QCO values that the terminator has - // to yield. Converting terminators in the same worklist would make the - // result depend on dialect-conversion traversal order. - target.addLegalOp(); - - // Register operation conversion patterns with state tracking. - patterns - .add( - typeConverter, context, &state); - - patterns.add(context); - - // Not part of the central gate table. - patterns.add>( - typeConverter, context, &state); - + }); + collectStructuredCaptures(moduleOp, state); + auto* ctx = moduleOp.getContext(); + QCToQCOTypeConverter converter(ctx); + RewritePatternSet patterns(ctx); + patterns + .add(converter, ctx, &state); + patterns.add>( + converter, ctx, &state); #define MQT_GATE(KEY, NAME, GETTER, TARGETS, PARAMS, SUFFIX, CTL_SUFFIX) \ patterns.add< \ ConvertQCGateToQCO>( \ - typeConverter, context, &state); + converter, ctx, &state); #include "mqt/Conversion/GateTable.def" - - // QC qubit arguments become QCO arguments plus trailing pass-through - // results. - patterns.add(typeConverter, context, &state); - target.addDynamicallyLegalOp([&](func::FuncOp op) { - return typeConverter.isSignatureLegal(op.getFunctionType()) && - typeConverter.isLegal(&op.getBody()); - }); - - // Convert returns until borrowed results and local resource releases - // have been emitted, even if the original result types are already legal. - patterns.add(typeConverter, context, &state); - target.addDynamicallyLegalOp([&](func::ReturnOp op) { - if (!typeConverter.isLegal(op)) { - return false; + FrozenRewritePatternSet frozen(std::move(patterns)); + PatternApplicator applicator(frozen); + applicator.applyDefaultCostModel(); + PatternRewriter rewriter(ctx); + SmallVector order, sources; + SmallVector sourceArguments; + moduleOp.walk( + [&](Operation* op) { order.push_back(op); }); + // Structural patterns move original children, never delete them. New ops + // need no worklist: each source operation is handled exactly once. + for (auto* operation : order) { + rewriter.setInsertionPoint(operation); + if (auto alloc = dyn_cast(operation)) { + if (failed(state.ensureAllocationMode(AllocationMode::Dynamic, alloc))) { + return failure(); } - if (state.functionQubitArguments.contains( - op->getParentOfType()) || - state.localReferenceRegisters.contains(op->getParentRegion())) { - return false; - } - const auto it = state.qubitMap.find(op->getParentRegion()); - return it == state.qubitMap.end() || it->second.empty(); - }); - - // Generic calls receive the pass-through results added to their callees. - patterns.add(typeConverter, context, &state); - target.addDynamicallyLegalOp( - [&](func::CallOp op) { return typeConverter.isLegal(op); }); - - // Convert structured parents and their contents first. - if (failed(applyPartialConversion(moduleOp, target, std::move(patterns)))) { - signalPassFailure(); - return; + auto out = qco::AllocOp::create(rewriter, alloc.getLoc()); + assignMappedQubit(state, out, alloc.getResult(), out.getResult()); + sources.push_back(alloc); + continue; } - // Source register values and loaded qubit references have been erased. - // Structured conversion state uses stable register identifiers from here - // on. - state.registerIds.clear(); - state.registerAccesses.clear(); - state.convertedQubitAliases.clear(); - state.regionQubitMap.clear(); - state.regionRegisterMap.clear(); - - if (state.structuredValues.empty()) { - return; + if (auto ref = dyn_cast(operation)) { + if (failed(state.ensureAllocationMode(AllocationMode::Static, ref))) { + return failure(); + } + if (state.registerAccesses.contains(ref.getQubit())) { + sources.push_back(ref); + continue; + } + auto out = qco::StaticOp::create(rewriter, ref.getLoc(), ref.getIndex()); + assignMappedQubit(state, out, ref.getQubit(), out.getQubit()); + sources.push_back(ref); + continue; } - ConversionTarget terminatorTarget(*context); - terminatorTarget.markUnknownOpDynamicallyLegal( - [](Operation*) { return true; }); - terminatorTarget.addDynamicallyLegalOp( - [&](Operation* op) { - auto* parentOp = op->getParentOp(); - if (!state.structuredValues.contains(parentOp)) { - return true; + bool required = + operation->getDialect() == ctx->getLoadedDialect(); + if (auto function = dyn_cast(operation)) { + required = !converter.isSignatureLegal(function.getFunctionType()); + if (required) { + for (auto argument : function.getArguments()) { + if (isa(argument.getType()) || + isQubitMemrefType(argument.getType())) { + sourceArguments.push_back(argument); } + } + } + } + if (isa(operation)) { + required = !converter.isLegal(operation); + if (required) { + sources.push_back(operation); + } + } + if (isa(operation)) { + auto* region = operation->getParentRegion(); + auto it = state.qubitMap.find(region); + required = !converter.isLegal(operation) || + state.functionQubitArguments.contains( + operation->getParentOfType()) || + state.localReferenceRegisters.contains(region) || + (it != state.qubitMap.end() && !it->second.empty()); + } + if (operation->getDialect() == + ctx->getLoadedDialect()) { + required = + llvm::any_of(operation->getOperandTypes(), isQubitMemrefType) || + llvm::any_of(operation->getResultTypes(), isQubitMemrefType); + if (required && + isa(operation)) { + sources.push_back(operation); + } + } + if (isa( + operation)) { + required = !state.regionQubitMap[operation].empty() || + !state.regionRegisterMap[operation].empty(); + } + if (isa(operation)) { + required = state.structuredValues.contains(operation->getParentOp()); + } + if (required && failed(applicator.matchAndRewrite(operation, rewriter))) { + return operation->emitError("cannot lower operation to QCO"); + } + } + for (auto* op : llvm::reverse(sources)) { + if (!op->use_empty()) { + return op->emitError("unconverted QC reference uses"); + } + rewriter.eraseOp(op); + } + for (auto argument : llvm::reverse(sourceArguments)) { + if (!argument.use_empty()) { + return moduleOp.emitError("unconverted QC argument uses"); + } + argument.getOwner()->eraseArgument(argument.getArgNumber()); + } + bool remains = false; + moduleOp.walk([&](Operation* op) { + remains |= op->getDialect() == ctx->getLoadedDialect(); + }); + return success(!remains); +} - if (auto condition = dyn_cast(op)) { - return llvm::equal(condition.getArgs().getTypes(), - parentOp->getResultTypes()); - } - if (auto whileOp = dyn_cast(parentOp)) { - return llvm::equal(op->getOperandTypes(), - whileOp.getInits().getTypes()); - } - return llvm::equal(op->getOperandTypes(), parentOp->getResultTypes()); - }); - - RewritePatternSet terminatorPatterns(context); - terminatorPatterns.add( - typeConverter, context, &state); - if (failed(applyPartialConversion(moduleOp, terminatorTarget, - std::move(terminatorPatterns)))) { +namespace { + +struct QCToQCO final : impl::QCToQCOBase { + using QCToQCOBase::QCToQCOBase; + +protected: + void runOnOperation() override { + if (failed(lowerToQCO(getOperation()))) { signalPassFailure(); } } diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index a80782170d..b2fc890333 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -941,6 +941,87 @@ TEST_F(QCToQCORegressionTest, ThreadsReferencesThroughExternalCalls) { EXPECT_TRUE(llvm::equal(calls[1].getOperands(), calls[0].getResults())); } +TEST_F(QCToQCORegressionTest, + RoundTripsInterleavedArgumentsAndOwnedCallResults) { + auto moduleOp = parseSourceString(R"mlir( + module { + func.func @main(%angle: f64, %flag: i1) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %r = qc.alloc : !qc.qubit + %theta, %owned, %bit = func.call @helper(%angle, %q, %flag, %r) + : (f64, !qc.qubit, i1, !qc.qubit) -> (f64, !qc.qubit, i1) + qc.ry(%theta) %owned : !qc.qubit + qc.swap %q, %owned : !qc.qubit, !qc.qubit + scf.if %bit { qc.x %r : !qc.qubit } + return + } + func.func private @helper(%a: f64, %q: !qc.qubit, %b: i1, %r: !qc.qubit) + -> (f64, !qc.qubit, i1) { + qc.x %q : !qc.qubit + qc.ry(%a) %r : !qc.qubit + %new = func.call @factory() : () -> !qc.qubit + return %a, %new, %b : f64, !qc.qubit, i1 + } + func.func private @factory() -> !qc.qubit + })mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto helper = moduleOp->lookupSymbol("helper"); + const auto originalType = helper.getFunctionType(); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*moduleOp))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + expectNoQCOperations(*moduleOp); + ASSERT_EQ(helper.getNumArguments(), 4U); + ASSERT_EQ(helper.getNumResults(), 5U); + ASSERT_TRUE(succeeded(runQCOToQCConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_EQ(helper.getFunctionType(), originalType); + auto x = *helper.getOps().begin(); + auto ry = *helper.getOps().begin(); + EXPECT_EQ(x->getOperand(0), helper.getArgument(1)); + EXPECT_EQ(ry->getOperand(0), helper.getArgument(3)); + auto ret = cast(helper.getBody().front().back()); + EXPECT_EQ(ret.getOperand(0), helper.getArgument(0)); + EXPECT_EQ(ret.getOperand(2), helper.getArgument(2)); + EXPECT_TRUE(ret.getOperand(1).getDefiningOp()); +} + +TEST_F(QCToQCORegressionTest, RoundTripsUnitaryCallInNestedRegions) { + auto moduleOp = parseSourceString(R"mlir( + module { + func.func @main(%condition: i1) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + scf.if %condition { + qc.inv (%a = %q) { + qc.call @flip(%a) : !qc.qubit + qc.yield + } : !qc.qubit + } + qc.x %q : !qc.qubit + return + } + func.func private @flip(%q: !qc.qubit) attributes {mqt.unitary} { + qc.x %q : !qc.qubit + return + } + })mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*moduleOp))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + ASSERT_TRUE(succeeded(runQCOToQCConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto function = moduleOp->lookupSymbol("main"); + auto conditional = *function.getOps().begin(); + auto inverse = *conditional.getThenRegion().getOps().begin(); + auto call = *inverse.getRegion().getOps().begin(); + EXPECT_EQ(call->getOperand(0), inverse.getRegion().front().getArgument(0)); + EXPECT_TRUE(mlir::mqt::isUnitaryFunction( + moduleOp->lookupSymbol("flip"))); +} + TEST_F(QCToQCORegressionTest, ConvertsQubitFunctionArgumentsToTrailingResults) { constexpr llvm::StringLiteral source = R"mlir( module { @@ -1404,6 +1485,35 @@ module { EXPECT_TRUE(sawExpectedDiagnostic); } +TEST_F(QCToQCORegressionTest, RejectsRegisterAliasingExposedByIndexFolding) { + auto moduleOp = parseSourceString(R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %reg = memref.alloc() : memref<2x!qc.qubit> + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %index = arith.addi %zero, %one : index + %q0 = memref.load %reg[%index] : memref<2x!qc.qubit> + %q1 = memref.load %reg[%one] : memref<2x!qc.qubit> + qc.swap %q0, %q1 : !qc.qubit, !qc.qubit + memref.dealloc %reg : memref<2x!qc.qubit> + return + } + })mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("distinct qubit operands"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*moduleOp))); + EXPECT_TRUE(sawExpectedDiagnostic); +} + TEST_P(QCToQCOTest, ProgramConversion) { const auto& [_, programBuilder, referenceBuilder, expectsCompleteTensorState, skipReferenceComparison] = GetParam(); From cab35bf3598b311200e70567bad5b7c0c47f2420 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:39:44 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qc-to-qco-on-boundary-verification.md | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/.agent/plans/qc-to-qco-on-boundary-verification.md b/.agent/plans/qc-to-qco-on-boundary-verification.md index 5896cd05b8..fc3e4e2d0b 100644 --- a/.agent/plans/qc-to-qco-on-boundary-verification.md +++ b/.agent/plans/qc-to-qco-on-boundary-verification.md @@ -1,40 +1,41 @@ # Source-ordered QC → QCO on boundary verification Status: complete. Source-ordered conversion and targeted normalization are -implemented and locally validated against the pinned base. End-to-end performance -is mixed; whole-file C++ lint and human review remain merge gates. +implemented and locally validated against the pinned base. End-to-end +performance is mixed; whole-file C++ lint and human review remain merge gates. ## Scope and decisions -The implementation changes only `mlir/lib/Conversion/QCToQCO/QCToQCO.cpp` -and `mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp`. -Use ordinary rewrite patterns in explicit source order. Keep source reference -identities alive until their users have been lowered; move nested regions -without destroying their original children. Preserve type conversion, static -normalization, supported input checks, and linear output validation. +The implementation changes only `mlir/lib/Conversion/QCToQCO/QCToQCO.cpp` and +`mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp`. Use ordinary rewrite +patterns in explicit source order. Keep source reference identities alive until +their users have been lowered; move nested regions without destroying their +original children. Preserve type conversion, static normalization, supported +input checks, and linear output validation. Reusing preflight state only when normalization is skipped did not improve the -selected workloads: OpenQASM register inputs always require normalization. -That experiment is not retained. Normalization can invalidate source identities, -so eliminating post-normalization collection needs a stronger invariant. +selected workloads: OpenQASM register inputs always require normalization. That +experiment is not retained. Normalization can invalidate source identities, so +eliminating post-normalization collection needs a stronger invariant. Sampling the factor247 conversion identified region liveness cleanup inside the greedy normalizer as a substantial cost. Use MLIR's existing `GreedySimplifyRegionLevel::Disabled` for normalization; this pass needs index folding and static-reference normalization, not control-flow optimization. -The baseline is PR #2698 at `2cf5d436150a6fe953fbc6d8f0c44f126bba3c1f`. -Keep its QCO → QC lowering, boundary-verification policy, mapping, and all other +The baseline is PR #2698 at `2cf5d436150a6fe953fbc6d8f0c44f126bba3c1f`. Keep its +QCO → QC lowering, boundary-verification policy, mapping, and all other pipelines unchanged. Existing ordered reverse-conversion work is outside scope. ## Validation Both variants use the Release preset, standalone native builds, and the same -assertion-free LLVM/MLIR 23.1.0 toolchain. Run the QCToQCO, QCOToQC, and Compiler -GoogleTest targets under `build/release/mlir/unittests/` to reproduce the native -checks. All 602 baseline tests and 605 final candidate tests pass. The three -added tests cover interleaved call signatures with owned results, unitary calls -inside nested regions, and register aliasing exposed by index folding. +assertion-free LLVM/MLIR 23.1.0 toolchain. Run the QCToQCO, QCOToQC, and +Compiler GoogleTest targets under `build/release/mlir/unittests/` to reproduce +the native checks. All 602 baseline tests and 605 final candidate tests pass. +The three added tests cover interleaved call signatures with owned results, +unitary calls inside nested regions, and register aliasing exposed by index +folding. AddressSanitizer with assertions passes all 174 forward-conversion tests. The converter and its test translation unit are instrumented; dependencies are not. @@ -42,18 +43,18 @@ Leak detection is disabled. Repository lint (`uvx nox -s lint`) and `git diff --check` pass. The performance comparison uses frozen Benchpress inputs from revision -`045e7ca6a8e5302826d82f07d42b5689ca61db3c`: QFT with 18 and 160 qubits, QRAM with -20 qubits, Ising with 26 qubits, cat with 260 qubits, W-state with 380 qubits, -and factor247 with 15 qubits. Seven unique circuits cover forward conversion; -seven circuit/topology pairs cover full compilation, excluding QFT-160 and -including QFT-18 on both all-to-all and linear connectivity. +`045e7ca6a8e5302826d82f07d42b5689ca61db3c`: QFT with 18 and 160 qubits, QRAM +with 20 qubits, Ising with 26 qubits, cat with 260 qubits, W-state with 380 +qubits, and factor247 with 15 qubits. Seven unique circuits cover forward +conversion; seven circuit/topology pairs cover full compilation, excluding +QFT-160 and including QFT-18 on both all-to-all and linear connectivity. The final direct comparison includes both changes against unmodified PR #2698, not a product of earlier incremental ratios. Each pair has three serial rounds with alternating variant order and seed 42. All 84 samples pass: 42 per variant, -without failures, errors, skips, timeouts, or interrupted samples. Source, input, -configuration, binary, and output hashes are checked. Every paired output IR -and returned gate count is identical, including repetitions. +without failures, errors, skips, timeouts, or interrupted samples. Source, +input, configuration, binary, and output hashes are checked. Every paired output +IR and returned gate count is identical, including repetitions. Forward conversion has seven complete pairs: geometric candidate/baseline ratio 0.616, with summed case means of 11.393 s before and 6.460 s after. Factor247 @@ -74,9 +75,9 @@ A fresh isolated wheel passes 977 focused Python tests with no skips: Forward-conversion timing excludes the input copy. Full timing includes the QC copy, QC → QCO, target compilation, and QCO → QC. QASM loading, target setup, -extra output verification, inspection, and serialization are outside timers. -The native targets use frozen FlexibleBackend connectivity and the common -`sx`, `x`, `rz`, `cz`, measurement, and reset basis without calibration metadata. +extra output verification, inspection, and serialization are outside timers. The +native targets use frozen FlexibleBackend connectivity and the common `sx`, `x`, +`rz`, `cz`, measurement, and reset basis without calibration metadata. ## Limits @@ -87,5 +88,5 @@ fixed-seed rounds on a shared desktop do not establish statistical significance for small differences. Two-qubit depth is not independently measured. Whole-changed-file C++ lint was attempted against the pinned revision but stopped before checking any files because clang-tidy 23 is unavailable locally. -The supported conversion subset is unchanged; this work does not add support -for unstructured control flow. +The supported conversion subset is unchanged; this work does not add support for +unstructured control flow. From 8f351b95c20e322fb8a4b102b52c657b4e4793bd Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 12 Oct 2026 10:05:33 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=A1=20Avoid=20quadratic=20QC=20argume?= =?UTF-8?q?nt=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append converted function arguments and erase the old prefix in bulk to avoid repeated shifting. Preserve argument order, attributes, locations, and source quantum identities until lowering finishes. Cover wide interleaved signatures and remapped register indices. Assisted-by: OpenAI Codex --- .../qc-to-qco-on-boundary-verification.md | 50 +++++++---- mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 45 +++++----- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 82 +++++++++++++++++++ 3 files changed, 140 insertions(+), 37 deletions(-) diff --git a/.agent/plans/qc-to-qco-on-boundary-verification.md b/.agent/plans/qc-to-qco-on-boundary-verification.md index fc3e4e2d0b..485627d988 100644 --- a/.agent/plans/qc-to-qco-on-boundary-verification.md +++ b/.agent/plans/qc-to-qco-on-boundary-verification.md @@ -1,8 +1,8 @@ # Source-ordered QC → QCO on boundary verification Status: complete. Source-ordered conversion and targeted normalization are -implemented and locally validated against the pinned base. End-to-end -performance is mixed; whole-file C++ lint and human review remain merge gates. +implemented and locally validated. End-to-end performance is mixed; whole-file +C++ lint and human review remain merge gates. ## Scope and decisions @@ -13,6 +13,12 @@ their users have been lowered; move nested regions without destroying their original children. Preserve type conversion, static normalization, supported input checks, and linear output validation. +Append a complete converted function argument list, redirect classical uses, and +retain the original quantum references until lowering finishes. Remove the old +argument prefix in one operation. Individual middle insertions and erasures were +quadratic for wide signatures; bulk replacement preserves interleaved argument +order, attributes, and locations without that cost. + Reusing preflight state only when normalization is skipped did not improve the selected workloads: OpenQASM register inputs always require normalization. That experiment is not retained. Normalization can invalidate source identities, so @@ -23,28 +29,44 @@ greedy normalizer as a substantial cost. Use MLIR's existing `GreedySimplifyRegionLevel::Disabled` for normalization; this pass needs index folding and static-reference normalization, not control-flow optimization. -The baseline is PR #2698 at `2cf5d436150a6fe953fbc6d8f0c44f126bba3c1f`. Keep its -QCO → QC lowering, boundary-verification policy, mapping, and all other -pipelines unchanged. Existing ordered reverse-conversion work is outside scope. +The historical benchmark baseline is PR #2698 at +`2cf5d436150a6fe953fbc6d8f0c44f126bba3c1f`. QCO → QC lowering, +boundary-verification policy, mapping, and all other pipelines remain outside +the scope of this patch. Existing ordered reverse-conversion work is not +included. ## Validation Both variants use the Release preset, standalone native builds, and the same assertion-free LLVM/MLIR 23.1.0 toolchain. Run the QCToQCO, QCOToQC, and Compiler GoogleTest targets under `build/release/mlir/unittests/` to reproduce -the native checks. All 602 baseline tests and 605 final candidate tests pass. -The three added tests cover interleaved call signatures with owned results, -unitary calls inside nested regions, and register aliasing exposed by index -folding. +the native checks. The current candidate passes 606 tests: 176 QCToQCO, 158 +QCOToQC, and 272 Compiler. Added tests cover interleaved call signatures with +owned results, unitary calls inside nested regions, register aliasing exposed by +index folding, wide interleaved signatures, and remapped register index +arguments. -AddressSanitizer with assertions passes all 174 forward-conversion tests. The +AddressSanitizer with assertions passes all 176 forward-conversion tests. The converter and its test translation unit are instrumented; dependencies are not. Leak detection is disabled. Repository lint (`uvx nox -s lint`) and `git diff --check` pass. -The performance comparison uses frozen Benchpress inputs from revision -`045e7ca6a8e5302826d82f07d42b5689ca61db3c`: QFT with 18 and 160 qubits, QRAM -with 20 qubits, Ising with 26 qubits, cat with 260 qubits, W-state with 380 +Bulk argument replacement also passes 138 differential comparisons against +`0dde327c1`, with identical output for forward conversion and roundtrips. These +cover nested structured control flow, register and scalar qubits, calls, +attributes, and helper declaration order. + +For private helpers with 10,000, 20,000, and 40,000 unused borrowed qubit +arguments, median whole-CLI time falls from 151/649/2156 ms to 24/37/67 ms. Each +size uses three serial, alternating before/after pairs with identical output, +running only `builtin.module(qc-to-qco)` through `mqt-cc --run-pipeline`. Both +binaries use the same Release toolchain. These synthetic timings include parsing +and output; they are not isolated pass timings or Benchpress results. + +The following historical performance comparison predates the bulk-argument fix +and does not measure the current source. It uses frozen Benchpress inputs from +revision `045e7ca6a8e5302826d82f07d42b5689ca61db3c`: QFT with 18 and 160 qubits, +QRAM with 20 qubits, Ising with 26 qubits, cat with 260 qubits, W-state with 380 qubits, and factor247 with 15 qubits. Seven unique circuits cover forward conversion; seven circuit/topology pairs cover full compilation, excluding QFT-160 and including QFT-18 on both all-to-all and linear connectivity. @@ -67,7 +89,7 @@ phase rises from 47.344 s to 56.035 s while its forward conversion falls from 10.561 s to 6.608 s. The cause of the end-to-end regression is not established. Keep all samples and do not claim a universal full-compilation speedup. -A fresh isolated wheel passes 977 focused Python tests with no skips: +The historical isolated wheel passed 977 focused Python tests with no skips: `test_mlir.py`, `test_mlir_loops.py`, `test_mlir_qiskit_translation.py`, `test_mlir_qiskit_target.py`, `test_mlir_parameter_binding.py`, and `test_mlir_integer_interchange.py`. Run them with `python -m pytest` and diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 295ad164bf..c21d39d714 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -976,20 +976,20 @@ struct ConvertFuncOp final : LoweringPattern { if (op.isExternal()) { return success(); } - // Retain source arguments until their QC users are lowered. Inserting - // each converted argument next to its source preserves interleaved order - // when the old arguments are erased at the end of the pass. - DenseMap convertedArguments; + // Append the full signature to preserve interleaved order without shifting + // arguments. Keep source QC identities until their users are lowered. auto& block = op.getBody().front(); - for (unsigned index : llvm::reverse(qubitArguments)) { - auto converted = block.insertArgument(index + 1, inputs[index], - originalArguments[index].getLoc()); - convertedArguments[originalArguments[index]] = converted; + const auto numOriginalArguments = block.getNumArguments(); + for (auto [source, type] : llvm::zip_equal(originalArguments, inputs)) { + auto converted = block.addArgument(type, source.getLoc()); + if (source.getType() == type) { + rewriter.replaceAllUsesWith(source, converted); + } } auto& map = getState().qubitMap[&op.getBody()]; auto& functionArguments = getState().functionQubitArguments[op]; for (unsigned index : qubitArguments) { - Value converted = convertedArguments.lookup(originalArguments[index]); + Value converted = block.getArgument(numOriginalArguments + index); if (auto reg = getState().registerIds.find(originalArguments[index]); reg != getState().registerIds.end()) { const auto id = reg->second; @@ -1993,7 +1993,8 @@ struct ConvertSCFConditionOp final : LoweringPattern { /// Convert QC references to QCO values in source order, threading quantum state /// through functions and structured control flow. /// -/// Lower terminators after preceding operations so they yield the final mapped values. +/// Lower terminators after preceding operations so they yield the final mapped +/// values. static LogicalResult lowerToQCO(ModuleOp moduleOp) { LoweringState preflight; if (failed(validateSupportedInput(moduleOp)) || @@ -2001,8 +2002,8 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { return failure(); } // Register indices still need normalization for alias validation. - const bool needsNormalization = !preflight.registerIds.empty() || - !staticsAlreadyNormalized(moduleOp); + const bool needsNormalization = + !preflight.registerIds.empty() || !staticsAlreadyNormalized(moduleOp); if (needsNormalization && failed(normalizeStaticQubits(moduleOp))) { return failure(); } @@ -2048,7 +2049,7 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { applicator.applyDefaultCostModel(); PatternRewriter rewriter(ctx); SmallVector order, sources; - SmallVector sourceArguments; + SmallVector> sourceArgumentBlocks; moduleOp.walk( [&](Operation* op) { order.push_back(op); }); // Structural patterns move original children, never delete them. New ops @@ -2081,13 +2082,9 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { operation->getDialect() == ctx->getLoadedDialect(); if (auto function = dyn_cast(operation)) { required = !converter.isSignatureLegal(function.getFunctionType()); - if (required) { - for (auto argument : function.getArguments()) { - if (isa(argument.getType()) || - isQubitMemrefType(argument.getType())) { - sourceArguments.push_back(argument); - } - } + if (required && !function.isExternal()) { + sourceArgumentBlocks.emplace_back(&function.getBody().front(), + function.getNumArguments()); } } if (isa(operation)) { @@ -2133,11 +2130,13 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { } rewriter.eraseOp(op); } - for (auto argument : llvm::reverse(sourceArguments)) { - if (!argument.use_empty()) { + for (auto [block, count] : sourceArgumentBlocks) { + if (llvm::any_of( + block->getArguments().take_front(count), + [](BlockArgument argument) { return !argument.use_empty(); })) { return moduleOp.emitError("unconverted QC argument uses"); } - argument.getOwner()->eraseArgument(argument.getArgNumber()); + block->eraseArguments(0, count); } bool remains = false; moduleOp.walk([&](Operation* op) { diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index b2fc890333..375a79add4 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -44,6 +44,7 @@ #include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/Diagnostics.h" #include "mlir/IR/DialectRegistry.h" +#include "mlir/IR/Location.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/Matchers.h" #include "mlir/IR/OperationSupport.h" @@ -987,6 +988,87 @@ TEST_F(QCToQCORegressionTest, EXPECT_TRUE(ret.getOperand(1).getDefiningOp()); } +TEST_F(QCToQCORegressionTest, PreservesWideInterleavedSignatures) { + constexpr unsigned numQubits = 1024; + std::string source = "module { func.func private @wide("; + for (unsigned i = 0; i < numQubits; ++i) { + if (i != 0) { + source += ", "; + } + const auto suffix = std::to_string(i); + source += + "%a" + suffix + ": f64 {test.position = " + std::to_string(2 * i) + + " : i32}, %q" + suffix + + ": !qc.qubit {test.position = " + std::to_string(2 * i + 1) + " : i32}"; + } + source += ") -> (f64, f64) { return %a0, %a" + std::to_string(numQubits - 1) + + " : f64, f64 } }"; + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto function = moduleOp->lookupSymbol("wide"); + const auto originalType = function.getFunctionType(); + const auto originalAttrs = function.getAllArgAttrs(); + SmallVector originalLocations; + for (auto argument : function.getArguments()) { + originalLocations.push_back(argument.getLoc()); + } + + ASSERT_TRUE(succeeded(runQCToQCOConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + ASSERT_EQ(function.getNumArguments(), 2 * numQubits); + ASSERT_EQ(function.getNumResults(), numQubits + 2); + EXPECT_EQ(function.getAllArgAttrs(), originalAttrs); + auto ret = cast(function.getBody().front().getTerminator()); + EXPECT_EQ(ret.getOperand(0), function.getArgument(0)); + EXPECT_EQ(ret.getOperand(1), function.getArgument(2 * (numQubits - 1))); + for (unsigned i = 0; i < numQubits; ++i) { + EXPECT_TRUE(function.getArgument(2 * i).getType().isF64()); + EXPECT_TRUE(isa(function.getArgument(2 * i + 1).getType())); + EXPECT_EQ(ret.getOperand(i + 2), function.getArgument(2 * i + 1)); + } + for (auto [argument, location] : + llvm::zip_equal(function.getArguments(), originalLocations)) { + EXPECT_EQ(argument.getLoc(), location); + } + ASSERT_TRUE(succeeded(runQCOToQCConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_EQ(function.getFunctionType(), originalType); + EXPECT_EQ(function.getAllArgAttrs(), originalAttrs); +} + +TEST_F(QCToQCORegressionTest, RemapsRegisterIndexArguments) { + auto moduleOp = parseSourceString(R"mlir( + module { + func.func private @helper(%i: index, %reg: memref<4x!qc.qubit>, + %angle: f64, %q: !qc.qubit) -> index { + %slot = memref.load %reg[%i] : memref<4x!qc.qubit> + qc.ry(%angle) %slot : !qc.qubit + qc.swap %slot, %q : !qc.qubit, !qc.qubit + return %i : index + } + })mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto function = moduleOp->lookupSymbol("helper"); + const auto originalType = function.getFunctionType(); + ASSERT_TRUE(succeeded(runQCToQCOConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + const auto extracts = llvm::to_vector(function.getOps()); + ASSERT_FALSE(extracts.empty()); + for (auto extract : extracts) { + EXPECT_EQ(extract.getIndex(), function.getArgument(0)); + } + ASSERT_TRUE(succeeded(runQCOToQCConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_EQ(function.getFunctionType(), originalType); + auto ret = cast(function.getBody().front().getTerminator()); + EXPECT_EQ(ret.getOperand(0), function.getArgument(0)); +} + TEST_F(QCToQCORegressionTest, RoundTripsUnitaryCallInNestedRegions) { auto moduleOp = parseSourceString(R"mlir( module { From e974c57a0e71df9d996ba3a6cb57412561a56cd1 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 15 Oct 2026 22:43:38 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=94=A7=20Refresh=20conversion=20valid?= =?UTF-8?q?ation=20after=20rebasing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Avoid temporary string allocations in the wide-signature regression and separate historical performance evidence from current-main validation. Retain upstream conditional register-analysis reuse after rebasing. Assisted-by: Codex --- .../qc-to-qco-on-boundary-verification.md | 43 +++++++++++-------- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 13 ++++-- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/.agent/plans/qc-to-qco-on-boundary-verification.md b/.agent/plans/qc-to-qco-on-boundary-verification.md index 485627d988..35140306d8 100644 --- a/.agent/plans/qc-to-qco-on-boundary-verification.md +++ b/.agent/plans/qc-to-qco-on-boundary-verification.md @@ -1,8 +1,7 @@ # Source-ordered QC → QCO on boundary verification -Status: complete. Source-ordered conversion and targeted normalization are -implemented and locally validated. End-to-end performance is mixed; whole-file -C++ lint and human review remain merge gates. +Status: implementation complete; current-main rebase validated. Historical +end-to-end performance is mixed. Human review remains a merge gate. ## Scope and decisions @@ -19,10 +18,10 @@ argument prefix in one operation. Individual middle insertions and erasures were quadratic for wide signatures; bulk replacement preserves interleaved argument order, attributes, and locations without that cost. -Reusing preflight state only when normalization is skipped did not improve the -selected workloads: OpenQASM register inputs always require normalization. That -experiment is not retained. Normalization can invalidate source identities, so -eliminating post-normalization collection needs a stronger invariant. +The rebase onto `575acd1b9` retains main's #2756 preflight-state reuse when +normalization is skipped. Recollect register accesses after normalization +because it can invalidate source identities. The historical selected OpenQASM +register workloads always normalized, so they do not measure this reuse path. Sampling the factor247 conversion identified region liveness cleanup inside the greedy normalizer as a substantial cost. Use MLIR's existing @@ -37,14 +36,22 @@ included. ## Validation -Both variants use the Release preset, standalone native builds, and the same -assertion-free LLVM/MLIR 23.1.0 toolchain. Run the QCToQCO, QCOToQC, and -Compiler GoogleTest targets under `build/release/mlir/unittests/` to reproduce -the native checks. The current candidate passes 606 tests: 176 QCToQCO, 158 -QCOToQC, and 272 Compiler. Added tests cover interleaved call signatures with -owned results, unitary calls inside nested regions, register aliasing exposed by -index folding, wide interleaved signatures, and remapped register index -arguments. +The 2026-10-15 rebase passes 619 native tests: 176 QCToQCO, 154 QCOToQC, and 289 +Compiler, with two expected macOS `/dev/full` skips. Executable docs, repository +lint, and whole-file C++ lint pass. The wide-signature test's string +construction was adjusted for current lint; all 176 forward-conversion tests +passed again. No benchmarks or sanitizer runs were repeated for this rebase. + +### Historical validation and performance + +Both historical variants use the Release preset, standalone native builds, and +the same assertion-free LLVM/MLIR 23.1.0 toolchain. Run the QCToQCO, QCOToQC, +and Compiler GoogleTest targets under `build/release/mlir/unittests/` to +reproduce the native checks. The pre-rebase candidate passed 606 tests: 176 +QCToQCO, 158 QCOToQC, and 272 Compiler. Added tests cover interleaved call +signatures with owned results, unitary calls inside nested regions, register +aliasing exposed by index folding, wide interleaved signatures, and remapped +register index arguments. AddressSanitizer with assertions passes all 176 forward-conversion tests. The converter and its test translation unit are instrumented; dependencies are not. @@ -107,8 +114,6 @@ This is a native API comparison on selected Benchpress inputs, not a run of the pytest-benchmark suite. Each fresh process performs one timed call, with no warmup or adaptive calibration and a 600-second whole-process deadline. Three fixed-seed rounds on a shared desktop do not establish statistical significance -for small differences. Two-qubit depth is not independently measured. -Whole-changed-file C++ lint was attempted against the pinned revision but -stopped before checking any files because clang-tidy 23 is unavailable locally. -The supported conversion subset is unchanged; this work does not add support for +for small differences. Two-qubit depth is not independently measured. The +supported conversion subset is unchanged; this work does not add support for unstructured control flow. diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 375a79add4..baa4e46c91 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -996,10 +996,15 @@ TEST_F(QCToQCORegressionTest, PreservesWideInterleavedSignatures) { source += ", "; } const auto suffix = std::to_string(i); - source += - "%a" + suffix + ": f64 {test.position = " + std::to_string(2 * i) + - " : i32}, %q" + suffix + - ": !qc.qubit {test.position = " + std::to_string(2 * i + 1) + " : i32}"; + source.append("%a") + .append(suffix) + .append(": f64 {test.position = ") + .append(std::to_string(2 * i)) + .append(" : i32}, %q") + .append(suffix) + .append(": !qc.qubit {test.position = ") + .append(std::to_string(2 * i + 1)) + .append(" : i32}"); } source += ") -> (f64, f64) { return %a0, %a" + std::to_string(numQubits - 1) + " : f64, f64 } }"; From 0c087068b5f49f0dd71d064dbfe08dbe432bf205 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 18 Oct 2026 21:11:45 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9A=A1=20Refine=20QC/QCO=20conversion=20?= =?UTF-8?q?and=20builder=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Combine the builder and iterator audit with source-order lowering. Own unfinished builder modules, enforce region and tensor-slot contracts, and preserve positional quantum state through mixed classical control flow. Release completed conversion state, avoid duplicate capture storage and validation, and traverse single-use consumers directly. Consolidate shared conversion fixtures while preserving the conversion and regression cases. Keep entry-block allocations, explicit quantum region arguments, and dynamic tensor-index equality as documented limits. Record measured memory improvements without claiming a general full-pipeline speedup. Assisted-by: GPT-6 via Codex --- .../audits/qc-qco-combined-2716-20261018.md | 162 +++++ .agent/audits/qc-qco-upstream-20261017.md | 202 ++++++ .../qc-to-qco-on-boundary-verification.md | 6 +- .../include/mqt/Conversion/QCToQCO/QCToQCO.td | 2 + .../mqt/Dialect/QC/Builder/QCProgramBuilder.h | 14 +- .../Dialect/QCO/Builder/QCOProgramBuilder.h | 28 +- mlir/include/mqt/Dialect/QCO/QCOUtils.h | 20 + .../mqt/Dialect/QCO/Utils/FunctionUtils.h | 1 + mlir/lib/Compiler/Pipeline.cpp | 5 +- mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 193 +++--- .../Dialect/QC/Builder/QCProgramBuilder.cpp | 40 +- .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 187 +++--- .../QCO/Transforms/Mapping/Mapping.cpp | 6 +- .../FuseSingleQubitUnitaryRuns.cpp | 28 +- .../MergeSingleQubitRotationGates.cpp | 13 +- mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp | 36 +- mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 23 +- .../Dialect/QTensor/Utils/TensorIterator.cpp | 63 +- .../Conversion/ConversionTestCases.h | 356 ++++++++++ .../Conversion/QCOToQC/test_qco_to_qc.cpp | 550 +--------------- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 612 ++---------------- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 13 +- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 54 ++ .../Dialect/QTensor/IR/test_qtensor_ir.cpp | 102 +-- .../QTensor/Utils/test_tensoriterator.cpp | 136 ++-- mlir/unittests/programs/qco_programs.cpp | 17 - mlir/unittests/programs/qco_programs.h | 8 - 27 files changed, 1291 insertions(+), 1586 deletions(-) create mode 100644 .agent/audits/qc-qco-combined-2716-20261018.md create mode 100644 .agent/audits/qc-qco-upstream-20261017.md create mode 100644 mlir/unittests/Conversion/ConversionTestCases.h diff --git a/.agent/audits/qc-qco-combined-2716-20261018.md b/.agent/audits/qc-qco-combined-2716-20261018.md new file mode 100644 index 0000000000..e78973818c --- /dev/null +++ b/.agent/audits/qc-qco-combined-2716-20261018.md @@ -0,0 +1,162 @@ +# Combined QC/QCO audit and PR #2716 + +Date: 2026-10-18. Combined implementation for PR #2716. + +## Decision + +Combine the [QC/QCO audit](qc-qco-upstream-20261017.md) with +[PR #2716](https://github.com/munich-quantum-toolkit/core/pull/2716). The audit +repairs builder ownership, region and tensor contracts, and consumer traversal; +the PR simplifies forward conversion. The changes preserve the same supported +input contract and share the consolidated conversion fixtures. + +The combined implementation is based on upstream `main` at +`470094596af808df39cd25563377534bd44c2129`. Upstream advanced during the +measurements; the frozen four-way comparison remains based on +`056bef24722dd0a1f0813f3f7b4197c10f8eb950`. The original PR head was +`9d929c5947ddb34bed3a9ef729635509d91674bf`, originally based on +`575acd1b9ca52e4608ccf4833e7d60640913cef4`. The sole application conflict was +the call-pattern signature beside an obsolete callee lookup. Keep main's removal +of that lookup and use the PR's ordinary pattern signature. The original audit +worktree remains unchanged. The subsequent upstream commits apply cleanly; their +only overlapping builder edit changes loop canonicalization walk order and is +retained. The latest main change only reorders lint hooks. The original PR +commits retain their authorship after rebasing; the audit and further +simplifications follow in a separate commit. + +## Further simplifications + +- Release preflight register provenance before normalization and rebuild it in + the same state afterward. Keep both validation phases: folding can reveal + aliases that were not apparent in the original indices. +- Move structured capture vectors out of their source SetVectors; erase the old + entries instead of retaining two copies and their membership tables. +- Resolve structured outputs through borrowed views into one reserved vector. + Release completed region mappings after copying terminator operands. Retain + shared output ordering while sibling regions still need it. +- Keep unitary attributes throughout lowering. The direct rewrite driver does + not verify partially converted IR, so temporary removal/restoration is unused. +- Refresh load provenance directly in the source-retention branch and cache + dialect handles. Remove the load-only pattern and the final QC scan: every + source QC operation must be rewritten or checked and erased. Boundary + verification and checks for remaining reference uses stay in place. + +These follow-up changes remove another 44 net converter lines from the initial +combination. Against upstream, the complete prototype removes 581 net +source/test lines: 146 production and 435 test lines. It preserves the shared +132 forward and 133 reverse conversion cases and all five PR regressions; this +follow-up adds no tests. Independent design and correctness reviews, including +the final ponytail review, found no remaining concrete issue or necessary +regression gap. The final review covered converter lifetimes and scaling, +builder ownership and callers, iterator correspondence, consumer traversal, and +public validation boundaries. + +The source-operation snapshot remains deliberate. Structural rewrites move +children, and deferred erasure preserves reference identity. Replacing it with a +custom mutation-aware walker would add lifetime rules without measured benefit. +The entry-block allocation limit, single-block structured input, tensor slot +contract, and positional quantum correspondence remain explicit constraints. + +## Measurement method + +Four variants isolate the contributions: audit alone, PR alone, initial +combination, and simplified combination. All use Clang 23.1.4, portable +LLVM/MLIR 23.1.0, release `-O3`, ThinLTO, and assertions on DGX Spark. Five +fresh-process rounds rotate variant order on CPU 0; factor247 retains three +complete rounds. Further factor247 repetitions stopped after external CPU +contention became pronounced; its incomplete fourth round is saved separately +and excluded. The host/core is not reserved. MLIR multithreading is disabled. +Medians are descriptive local measurements, not a statistical claim for small +changes. Other compiler, lint, and benchmark jobs were observed on the host, +including CPU 0; wall-clock results are diagnostic. + +The native probe times `QCProgram::intoQCO`; full-pipeline cases also time +`compileForTarget` and `intoQC`. Import, initial copy, target construction, +extra output checks, inspection, and serialization are outside timing. Peak RSS +includes import and the retained source/copy, and is sampled before output +serialization. Output IR hashes and gate counts must agree across every variant +and repetition. Hashing in the parent uses streaming to avoid inflating child +RSS; an interrupted initial run with an inflated RSS floor is saved separately +and excluded. + +Seven frozen QASMBench circuits come from Benchpress +`36d550d38fe03fdbb2f00f045942b4315d9e4864`; four generated MLIR cases isolate +flat gates, register loads, sibling loops, and wide helper signatures. Full +compilation uses seed 42, ten mapping trials, one iteration, and the +`sx/x/rz/cz/measure/reset` basis without calibration metadata. Results do not +replace the PR's historical broader topology campaign. + +## Results + +All 252 retained samples pass output verification and agree on output IR hashes +and gate counts within each case: 212 forward-conversion and 40 full-pipeline +samples. No additional test case was needed. + +Forward-conversion medians follow. RSS columns compare the initial and +simplified combination. The Audit and PR columns separate their contributions. + +| Case | Audit (ms) | PR (ms) | Combined (ms) | Simplified (ms) | Combined RSS (MiB) | Simplified RSS (MiB) | +| ------------------- | ---------: | --------: | ------------: | --------------: | -----------------: | -------------------: | +| qft_n18 | 13.113 | 8.015 | 8.068 | 7.807 | 18.57 | 18.57 | +| qram_n20 | 1.487 | 0.893 | 0.922 | 0.892 | 16.35 | 16.54 | +| ising_n26 | 4.964 | 3.114 | 3.111 | 3.055 | 17.38 | 17.38 | +| factor247_n15 | 17702.400 | 11910.900 | 10539.000 | 11205.000 | 1722.50 | 1702.43 | +| cat_n260 | 14.662 | 10.188 | 10.215 | 10.085 | 18.32 | 18.32 | +| wstate_n380 | 43.493 | 29.210 | 29.218 | 28.839 | 22.41 | 22.44 | +| qft_n160 | 1404.180 | 856.447 | 873.410 | 823.506 | 196.03 | 190.62 | +| flat-1m | 1873.690 | 835.906 | 1188.950 | 818.760 | 492.00 | 492.07 | +| register-loads-100k | 1245.130 | 805.668 | 770.021 | 751.645 | 172.32 | 166.30 | +| sibling-loops-2k-32 | 185.160 | 103.468 | 102.871 | 93.460 | 64.99 | 59.27 | +| wide-helper-8192 | 7.503 | 2.564 | 2.583 | 2.613 | 17.72 | 17.91 | + +The further cleanup reduces peak RSS by 3.5% for register loads and 8.8% for +sibling loops. QFT-160 saves 5.4 MiB; factor247 saves 20.1 MiB. The flat +million-gate case is unchanged: imported/copied IR dominates its peak. + +The broader combination has larger memory effects: audit-only versus final peak +RSS falls from 736.3 to 492.1 MiB for one million flat gates, and from 259.5 to +190.6 MiB for QFT-160. These are process peaks for this harness, not +steady-state converter allocations. + +Full pipeline medians (QFT-18 linear; Ising-26 all-to-all): + +| Case | Audit (ms) | PR (ms) | Combined (ms) | Simplified (ms) | Combined RSS (MiB) | Simplified RSS (MiB) | +| --------- | ---------: | ------: | ------------: | --------------: | -----------------: | -------------------: | +| qft_n18 | 187.668 | 183.307 | 182.307 | 182.356 | 23.23 | 23.29 | +| ising_n26 | 22.152 | 20.375 | 20.250 | 20.218 | 20.41 | 20.41 | + +The further cleanup gives no meaningful full-pipeline improvement in these two +cases. Wall-time differences under contention do not establish small speedups or +regressions; in particular, factor247's simplified median is slower than the +initial combination's. Do not multiply component ratios or claim a universal +compiler speedup. The PR's historical factor247/square regression was not +remeasured here. + +## Validation and artifacts + +- The final `470094596` full `release-clang-ipo` build and CTest pass: 4,030 + passed, one expected `ScQDMIJobSpecificationTest.QueryJobId` skip; 2,925 MLIR + tests included. This includes the new upstream OpenQASM and jeff cases. +- Both full-pipeline cases were rerun with the refreshed binary; output IR + hashes and gate counts match the frozen comparison. +- Repository whole-file C++ lint passes for its 19 selected files. The final + capture-vector transfer also passes a complete converter translation-unit + recheck with no source diagnostics. +- MLIR reference generation, repository lint, and final diff checks pass. +- The production and test files are byte-identical to the reviewed and + C++-linted `8ed992b64` combination. Rebasing onto `470094596` adds only a + lint-hook ordering change; repository lint also passes with that change. +- Python bindings and sanitizer builds were not run for this combined + implementation. Hosted checks on the original PR head do not validate the + combination; final-head CI status is tracked on PR #2716. + +The earlier builder change makes builders move-only; borrowed contexts must +outlive their builders and IR. No in-tree caller needs builder copies. The +prototype does not broaden conversion support or promise a general full-compiler +speedup. + +External artifacts are in +`/home/nvidia/.codex/experiments/core-qc-qco-combined-20261018/measurements/`: +source patches, native probe/build/runner scripts, generated inputs, +environment, input/binary hashes, raw samples, and summaries. The combined +worktree is its sibling `core/`; `pr-only/` supplies the separate PR baseline. diff --git a/.agent/audits/qc-qco-upstream-20261017.md b/.agent/audits/qc-qco-upstream-20261017.md new file mode 100644 index 0000000000..34de709a2d --- /dev/null +++ b/.agent/audits/qc-qco-upstream-20261017.md @@ -0,0 +1,202 @@ +# QC/QCO builder and conversion audit + +Baseline: upstream `main` at `056bef24722dd0a1f0813f3f7b4197c10f8eb950`. Date: +2026-10-18. Status: fixes applied and locally validated. The initial audit patch +was carried forward from `6b6efeec9` without conflicts. + +This record covers the audit before integration with PR #2716. See the +[combined prototype](qc-qco-combined-2716-20261018.md) for the current result. + +Scope: QC/QCO program builders, both conversions, wire/tensor traversal, +function correspondence, and their tests and immediate consumers. Work started +from a clean isolated checkout. LLVM/MLIR 23.1 and Clang 23 were used. + +## Findings and changes + +1. **Nested QC allocations could crash conversion.** A valid `func.func` with an + outer `qc.alloc` and a second allocation inside `scf.if` reached the + `lookupMappedQubit` assertion through `mqt-cc --emit=qco` (exit 134). + `validateSupportedInput` now requires quantum allocations in a function's + entry block and diagnoses unsupported input before rewriting. A focused test + covers scalar allocation, register allocation, and stack allocation, and + checks that rejected IR is unchanged. The pass reference states the limit. + +2. **While iterators assumed the wrong correspondence.** `WireIterator` used raw + operand/result positions although classical loop state may change shape; + `TensorIterator` walked the before-region and failed when a borrowed tensor + passed through a helper call. The existing while test now exercises different + classical positions, a scalar wire, two tensor sizes, and a helper call. + Before the fix it selected a classical result as a qubit and then aborted. + Both iterators now use the quantum subsequence correspondence established by + `qco::verifyLinearity`; recursive tensor-body traversal is deleted. + +3. **Builder bookkeeping allowed invalid quantum captures and escapes.** Tracked + values could be consumed from the wrong region, and implicit cleanup could + use extracted values after their region ended. Shared validation now checks + the insertion region. Explicit argument/result transfers bypass that scope + check while retaining ownership checks. Single-target modifiers reuse their + range overloads. One focused rejection test covers scalar and tensor + captures, extracted-value escape, and modifier capture. + +4. **Tensor insertion did not enforce its existing slot contract.** + `QTensorOps.td` requires reinsertion into the original register and slot. The + program builder now rejects foreign/fresh scalars and unequal constant slots + using its existing tracking. Public builder documentation states the + contract. One rejection test covers these three cases. Dynamic index equality + and disjoint outstanding extractions remain program preconditions; no second + whole-IR slot analysis was added. + +5. **QC finalization disposed returned allocations.** `finalize(returnValues)` + now excludes directly returned scalar and register allocations from automatic + disposal, matching the existing function-building behavior. The allocation + order test now also checks retained return values. Returning loaded register + elements does not acquire new alias-analysis support. + +6. **Call-origin collection repeated argument scans.** `collectWireOrigins` now + resolves a callee and collects quantum argument indices once per call, then + maps matching trailing results. This preserves fresh leading results; tying + every quantum operand to every quantum result would be incorrect. The + existing tensor-lifetime recursion limit is documented and applies to depths + greater than or equal to 64. + +7. **Gate and measurement scans traversed rejected operations.** Rotation + merging, single-qubit fusion, and measurement routing now inspect direct + single-use consumers. They stop at the same boundaries without asking an + iterator to translate loop or call results first. Fusion deletion follows the + already-scanned chain backward. Existing boundary, synthesis, and routing + tests cover these paths. + +8. **Unfinished builders leaked their modules.** QC and QCO builders held raw + module pointers without a destructor. OpenQASM emission failures and + exhausted emission budgets can return before finalization; Qiskit import can + also throw during construction. Both builders now use `OwningOpRef` + and release ownership in `finalize()`. The owning handle also replaces the + separate context sentinel. Builders are move-only; their borrowed MLIR + context must outlive the builder and its IR. No in-tree callers require + copying builders. + +9. **Flat verification retained every wire origin unnecessarily.** The existing + linear-use walk now detects quantum function arguments or non-module regions. + Only these cases need the second positional walk and its origin map. Static + placement, uniqueness, and every linear-use check still run. Public + `collectWireOrigins` remains complete for conversion consumers; unitary + correspondence uses its already-verified quantum views without copying them. + +10. **Builder calls and tensor cleanup repeated bookkeeping.** Calls now pass + mixed operands directly to `prepareInitArgs`, removing the quantum gather + vector and scatter loop. Scalar-only boundaries no longer allocate a scalar + membership set needed only for register alias checks. Implicit reinsertion + builds its local tensor chain and updates tensor tracking once. Per-qubit + validation, deterministic order, and slot ownership checks remain intact. + +11. **The default pipeline repeated successful conversion validation.** QC, + OpenQASM, and jeff conversion paths already end with QCO linearity + validation. The immediate duplicate check is removed. Existing QCO inputs + still get validated because the public API exposes mutable IR. + +## Test reduction + +The final source/test diff removes 617 net lines, including 632 test lines. + +- Share 119 identical QC/QCO builder pairs between the two conversion suites. + All 132 QC-to-QCO and 133 QCO-to-QC cases retain their names, builders, flags, + and equivalence assertions. Independent mechanical comparison confirmed the + case multisets. Only suite prefixes change to common/special cases. +- Remove the always-false reference-comparison skip flag. +- Remove two tensor scenarios that moved qubits between slots contrary to the + documented contract. The builder rejection test retains the relevant negative + check. Remove their obsolete fixture and swap-target option. +- Replace an invalid, self-comparing index-mismatch fixture with a distinct + constant-index row in the existing adjacent-index test. Consolidate its three + former tests into one four-row table. Equal constants, distinct constants, + identical dynamic values, and potentially aliasing dynamic values each retain + explicit folding expectations and now check linearity before and after. +- Extend existing tests for returned allocations and while traversal. Only the + three focused builder/conversion rejection tests are new. + +## Design limits and overlap + +- Quantum values cross regions explicitly; classical captures remain supported. +- Positional quantum correspondence is a verified QCO contract. Historical + scalar-permutation assumptions do not override the current verifier. +- General while crossing scans the signature in linear time. Mapping still needs + that correspondence; no cache with IR-mutation invalidation was added. No + end-to-end speedup is attributed to the iterator changes. +- The original audit left the QC-to-QCO source-order rewrite to PR #2716. The + linked follow-up combines both. Direct QCO export redesign remains outside + this builder/conversion audit. + +## Performance follow-up + +The baseline is the refreshed upstream commit plus the initial audit patch; +measurements compare findings 8–11 against that baseline. Both binaries use the +same release preset: Clang 23.1.4, portable LLVM/MLIR 23.1.0, `-O3`, ThinLTO, +and assertions. The DGX Spark probe was pinned to Cortex-X925 CPU 0, with seven +alternating baseline/optimized process pairs. The host and core were not +reserved. These are workload-specific local measurements. + +The external C++ harness builds identical IR, runs ordinary verification outside +timing, and times three or five linearity checks per process. It covers 10,000, +100,000, and 1,000,000 gates over one or 128 wires, borrowed helper arguments, +structured regions, wide calls, and extracted-register cleanup. All completed +modules pass ordinary and linearity verification. Values below are medians. + +| Workload and measurement | Baseline | Updated | +| --------------------------------------------------------- | ---------: | ---------: | +| 1,000,000 flat gates, one wire: linearity verification | 140.91 ms | 11.87 ms | +| Same process: peak resident memory | 209.07 MiB | 152.20 MiB | +| 1,000,000 flat gates, 128 wires: linearity verification | 145.31 ms | 12.56 ms | +| 100,000 gates in borrowed helper, 128 wires: verification | 13.10 ms | 10.87 ms | +| 100,000 gates in a loop, 128 wires: verification | 13.11 ms | 10.83 ms | +| 5,000 mixed classical/64-qubit calls: construction | 43.29 ms | 35.01 ms | +| 100,000 extracted register slots: finalization | 98.06 ms | 96.51 ms | + +Tensor-finalization ranges overlap; no meaningful speedup is claimed for the +batched tracking change. Scalar gate construction was about 1–4% slower in these +runs, with overlapping ranges for the million-gate case; it is not a claimed +optimization. Iterator-returning tracking validators and additional caches were +left out: they need separate evidence to justify their extra API machinery. + +Twenty abandoned 20,000-gate builders retained 51,214,288 bytes for QC and +57,614,416 bytes for QCO before the fix. Afterward the retained allocator bytes +were 4,496 and 5,120 respectively. Increasing to 100 abandoned builders left +those values unchanged. The context is warmed and remains alive during this +measurement; this checks cleanup rather than allocator RSS release. + +Complete `mqt-cc --emit=qco` runs on 100,000-gate inputs showed much smaller +changes: QC input took 379.17 → 368.83 ms, and OpenQASM took 409.04 → 399.59 ms. +Peak memory was essentially unchanged. Baseline and updated output files had +identical SHA-256 hashes for each input format. These runs include parsing, +conversion, verification, formatting, and writing to `/dev/null`; they do not +establish a general compiler speedup. + +Reproduction artifacts remain outside the repository in +`/home/nvidia/.codex/experiments/core-qc-qco-audit-20261017/performance-20261018/`: +`prior-audit.patch`, `probe.cpp`, `build-probe.py`, `run-probes.py`, +`measurements.csv`, `run-cli.py`, `cli-measurements.csv`, and +`environment.json`. + +## Validation + +- Full `release-clang-ipo` configure/build passed. One Clang frontend crash + reported invalid bytes in an intact generated header; retry passed without a + source workaround. +- `ctest --preset release-clang-ipo -j6 --output-on-failure`: 4,030 passed, one + expected `ScQDMIJobSpecificationTest.QueryJobId` skip, no failures; 41.96 + seconds, including 2,925 MLIR tests and all 265 conversion cases. +- `uvx nox -s lint`: passed. Subsequent ownership documentation and this audit + also passed their relevant hooks. +- `uvx nox -s cpp-lint`: passed all 19 eligible changed C++ files, with zero + formatting or clang-tidy findings. +- `cmake --build --preset release-clang-ipo --target mlir-doc`: passed. +- External probes verify module cleanup, move-only builder types, successful IR + verification, and byte-identical compiler output. No new repository tests were + added for the performance follow-up. Existing tests cover non-positional + function/region results, static/use-count failures, mixed calls, borrowed + registers, implicit reinsertion, and mutable QCO pipeline inputs. +- Specialist design review and independent cross-review found no remaining + correctness issue in ownership transfer, positional checks, or the removal of + repeated validation. + +No hosted CI or Python suite was run. Changes remain local; nothing was +committed or published. diff --git a/.agent/plans/qc-to-qco-on-boundary-verification.md b/.agent/plans/qc-to-qco-on-boundary-verification.md index 35140306d8..bdff92c814 100644 --- a/.agent/plans/qc-to-qco-on-boundary-verification.md +++ b/.agent/plans/qc-to-qco-on-boundary-verification.md @@ -1,7 +1,9 @@ # Source-ordered QC → QCO on boundary verification -Status: implementation complete; current-main rebase validated. Historical -end-to-end performance is mixed. Human review remains a merge gate. +This plan records PR #2716 before combination with the QC/QCO audit. See the +[combined prototype](../audits/qc-qco-combined-2716-20261018.md) for current +source, validation, and performance. Historical end-to-end performance below is +mixed. Human review remains a merge gate. ## Scope and decisions diff --git a/mlir/include/mqt/Conversion/QCToQCO/QCToQCO.td b/mlir/include/mqt/Conversion/QCToQCO/QCToQCO.td index c7b84aa883..e41a599aa4 100644 --- a/mlir/include/mqt/Conversion/QCToQCO/QCToQCO.td +++ b/mlir/include/mqt/Conversion/QCToQCO/QCToQCO.td @@ -17,6 +17,8 @@ def QCToQCO : Pass<"qc-to-qco", "mlir::ModuleOp"> { Control flow must use structured SCF operations. Operations with block successors, including `cf.br`, `cf.cond_br`, and `cf.switch`, are diagnosed before conversion. + Quantum allocations must be in a function's entry block. Nested regions + may use outer allocations through the supported SCF operations. Private helpers may borrow fixed-size rank-one qubit memrefs with identity layout and default memory space. They become qubit tensors. Updated scalar qubits and complete tensors are appended to ordinary results in argument diff --git a/mlir/include/mqt/Dialect/QC/Builder/QCProgramBuilder.h b/mlir/include/mqt/Dialect/QC/Builder/QCProgramBuilder.h index aeb658ffa4..7c527d39b9 100644 --- a/mlir/include/mqt/Dialect/QC/Builder/QCProgramBuilder.h +++ b/mlir/include/mqt/Dialect/QC/Builder/QCProgramBuilder.h @@ -17,6 +17,7 @@ #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" #include "mlir/IR/OwningOpRef.h" #include "mlir/IR/Value.h" #include "mlir/Support/LLVM.h" @@ -32,8 +33,6 @@ namespace mlir { // Forward declarations class MLIRContext; -class ModuleOp; -class Operation; class ValueRange; namespace qc { @@ -43,7 +42,9 @@ namespace qc { /// The QCProgramBuilder provides a type-safe interface for constructing /// quantum circuits using reference semantics. Operations modify qubits in /// place without producing new SSA values, providing a natural mapping to -/// hardware execution models. +/// hardware execution models. The builder destroys unfinished IR on +/// destruction; `finalize()` transfers ownership to its caller. Builders are +/// move-only. The borrowed MLIR context must outlive the builder and its IR. /// /// @par Qubit addressing: /// A program must use either static qubits (`staticQubit`) or dynamic @@ -1331,8 +1332,8 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { /// @param returnValues Values representing the return values of the main /// function. /// - /// Automatically deallocates all remaining valid qubits and tensors of - /// qubits, adds a return statement with the given return values, and + /// Deallocates owned qubits and registers except those returned directly, + /// adds a return statement with the given return values, and /// transfers ownership of the module to the caller. The builder should not be /// used after calling this method. /// @@ -1369,8 +1370,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { private: enum class AllocationMode : uint8_t { Unset, Static, Dynamic }; - MLIRContext* ctx{}; - Operation* moduleOp_; + OwningOpRef moduleOp_; /// Track allocated qubits for automatic deallocation SetVector allocatedQubits; diff --git a/mlir/include/mqt/Dialect/QCO/Builder/QCOProgramBuilder.h b/mlir/include/mqt/Dialect/QCO/Builder/QCOProgramBuilder.h index 910bc87ae5..8e62c377b2 100644 --- a/mlir/include/mqt/Dialect/QCO/Builder/QCOProgramBuilder.h +++ b/mlir/include/mqt/Dialect/QCO/Builder/QCOProgramBuilder.h @@ -16,6 +16,7 @@ #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" #include "mlir/IR/OwningOpRef.h" #include "mlir/IR/Value.h" #include "mlir/Support/LLVM.h" @@ -32,7 +33,6 @@ namespace mlir { // Forward declarations class MLIRContext; -class ModuleOp; class Operation; class ValueRange; @@ -42,7 +42,10 @@ namespace qco { /// /// Operations consume qubit SSA values and return their replacements. The /// builder tracks live values and terminates with a usage error when a -/// consumed or untracked value is reused. +/// consumed or untracked value is reused. The builder destroys unfinished IR +/// on destruction; `finalize()` transfers ownership to its caller. Builders +/// are move-only. The borrowed MLIR context must outlive the builder and its +/// IR. /// /// @par Qubit addressing: /// A program must use either static qubits (`staticQubit`) or dynamic @@ -53,6 +56,8 @@ namespace qco { /// `mqt.entry_point` function. Helpers receive allocated qubits as arguments. /// /// @par Structured control flow: +/// Quantum values must enter regions through explicit arguments; classical +/// captures remain supported. /// These rules apply to linear results; `qcoIf` may prepend classical results. /// Callbacks for `qcoIf`, `qcoIndexSwitch`, `scfFor`, and `scfWhile` must /// preserve each input's type and qubit or register identity by result @@ -381,6 +386,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { /// extracted qubit is added to the qubit tracking and the tracking of the /// source tensor is updated. /// + /// The index must identify an unextracted slot; dynamic index equality is a + /// program precondition. + /// /// @param tensor Source tensor (must be valid/unconsumed) /// @param index The index from where the qubit is extracted /// @return Pair of (outTensor, extractedQubit) @@ -401,6 +409,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { /// given index. The inserted qubit is consumed and removed from the qubit /// tracking while the tracking for the source tensor is updated. /// + /// The scalar must return to its original register and slot; equality of + /// dynamic extraction and insertion indices is a program precondition. + /// /// @param scalar The scalar qubit that is inserted (must be valid/unconsumed) /// @param tensor The tensor where the qubit is inserted (must be /// valid/unconsumed) @@ -1814,8 +1825,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { private: enum class AllocationMode : uint8_t { Unset, Static, Dynamic }; - MLIRContext* ctx{}; - Operation* moduleOp_; + OwningOpRef moduleOp_; /// Check if the builder has been finalized void checkFinalized() const; @@ -1828,13 +1838,14 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { /// /// @param qubit Qubit value to validate /// Terminates with a usage error if the qubit is consumed or untracked. - void validateQubitValue(Value qubit) const; + void validateQubitValue(Value qubit, bool checkScope = true) const; /// Update tracking when an operation consumes and produces a qubit /// /// @param inputQubit Input qubit being consumed (must be valid) /// @param outputQubit New output qubit being produced - void updateQubitTracking(Value inputQubit, Value outputQubit); + void updateQubitTracking(Value inputQubit, Value outputQubit, + bool checkScope = true); /// Count unique tensors int64_t tensorCounter = 0; @@ -1858,13 +1869,14 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { /// /// @param tensor Tensor value to validate /// Terminates with a usage error if the tensor is consumed or untracked. - void validateTensorValue(Value tensor) const; + void validateTensorValue(Value tensor, bool checkScope = true) const; /// Update tracking when an operation consumes and produces a tensor /// /// @param inputTensor Input tensor being consumed (must be valid) /// @param outputTensor New output tensor being produced - void updateTensorTracking(Value inputTensor, Value outputTensor); + void updateTensorTracking(Value inputTensor, Value outputTensor, + bool checkScope = true); /// Dispose of every live linear value in the current function. void disposeLinearValues(); diff --git a/mlir/include/mqt/Dialect/QCO/QCOUtils.h b/mlir/include/mqt/Dialect/QCO/QCOUtils.h index 524ae12c10..abda0dc053 100644 --- a/mlir/include/mqt/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mqt/Dialect/QCO/QCOUtils.h @@ -21,7 +21,9 @@ #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/ErrorHandling.h" #include #include @@ -57,6 +59,24 @@ inline bool checkDeadGate(Operation* op) { isa(shapedType.getElementType()); } +/// Return the corresponding quantum value, ignoring classical positions. +/// +/// Requires positional quantum signatures validated by @ref verifyLinearity. +/// Runs in linear time in the input and output signature widths. +[[nodiscard]] inline Value getCorrespondingQuantumValue(ValueRange inputs, + ValueRange outputs, + unsigned inputIndex) { + auto ordinal = llvm::count_if(inputs.take_front(inputIndex), [](Value value) { + return isLinearQubitType(value.getType()); + }); + for (auto output : outputs) { + if (isLinearQubitType(output.getType()) && ordinal-- == 0) { + return output; + } + } + llvm_unreachable("expected corresponding quantum value"); +} + /// Verify linear uses, static roots, and positional quantum state flow. /// /// Requires ordinary MLIR verification first. Every linear QCO value has diff --git a/mlir/include/mqt/Dialect/QCO/Utils/FunctionUtils.h b/mlir/include/mqt/Dialect/QCO/Utils/FunctionUtils.h index bac877b763..4b036b902b 100644 --- a/mlir/include/mqt/Dialect/QCO/Utils/FunctionUtils.h +++ b/mlir/include/mqt/Dialect/QCO/Utils/FunctionUtils.h @@ -50,5 +50,6 @@ namespace mlir::qco { /// /// Slot identity and disjoint extractions are program preconditions; known /// violations and positional region correspondence are checked separately. +/// Returns false at nesting depth 64 or greater to bound recursive stack use. [[nodiscard]] bool hasCompleteTensorLifetime(Value tensor, unsigned depth = 0); } // namespace mlir::qco diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index 66d9d96348..0eaa25aaeb 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -464,6 +464,9 @@ runDefaultPipelineImpl(CompilerInput&& program, ProgramFormat output, [](T&& value) -> FailureOr { using ProgramType = std::remove_cvref_t; if constexpr (std::is_same_v) { + if (failed(qco::verifyLinearity(value.module()))) { + return failure(); + } return std::forward(value); } else if constexpr (std::is_same_v) { auto qc = QCProgram::fromOpenQASMString(value.source()); @@ -476,7 +479,7 @@ runDefaultPipelineImpl(CompilerInput&& program, ProgramFormat output, } }, std::move(program)); - if (failed(qco) || failed(qco::verifyLinearity(qco->module()))) { + if (failed(qco)) { return failure(); } if (output == ProgramFormat::QCO) { diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index c21d39d714..183384e042 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -12,7 +12,6 @@ #include "mqt/Dialect/CBit/IR/CBitDialect.h" #include "mqt/Dialect/CBit/IR/CBitOps.h" -#include "mqt/Dialect/MQT/IR/MQTDialect.h" #include "mqt/Dialect/QC/IR/QCDialect.h" #include "mqt/Dialect/QC/IR/QCInterfaces.h" #include "mqt/Dialect/QC/IR/QCOps.h" @@ -46,11 +45,10 @@ #include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/RegionUtils.h" +#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/MapVector.h" #include "llvm/ADT/STLExtras.h" -#include "llvm/ADT/ScopeExit.h" -#include "llvm/ADT/TypeSwitch.h" #include #include @@ -133,9 +131,6 @@ struct LoweringState { DenseMap> regionRegisterMap; /// Original QC value order for already-converted structured operations. - /// - /// Kept separate from the legality maps above so converted SCF operations - /// remain legal. DenseMap structuredValues; /// Qubit keys yielded by converted modifier regions, in yield order. @@ -282,16 +277,6 @@ resolveMappedQubits(LoweringState& state, Operation* anchor, }); } -/// Resolves a range of QC memrefs to their latest QTensor values. -template -[[nodiscard]] static SmallVector -resolveMappedTensors(LoweringState& state, Operation* anchor, - const Range& registers) { - return llvm::map_to_vector(registers, [&](RegisterId reg) { - return lookupMappedTensor(state, anchor, reg); - }); -} - /// Updates mappings for matching QC and QCO qubit ranges. template static void assignMappedQubits(LoweringState& state, Operation* anchor, @@ -395,23 +380,26 @@ static void commitQubits(LoweringState& state, Operation* anchor, /// Resolves all structured QC state to QCO and QTensor values. [[nodiscard]] static SmallVector resolveAllValues(LoweringState& state, Operation* anchor) { - SmallVector registers; - SmallVector qcQubits; + ArrayRef registers; + ArrayRef qcQubits; auto* owner = structuredValueOwner(anchor); if (const auto it = state.structuredValues.find(owner); it != state.structuredValues.end()) { - llvm::append_range(registers, it->second.registers); - llvm::append_range(qcQubits, it->second.qubits); + registers = it->second.registers; + qcQubits = it->second.qubits; } else { - llvm::append_range(registers, state.regionRegisterMap[owner]); - llvm::append_range(qcQubits, state.regionQubitMap[owner]); + registers = state.regionRegisterMap[owner].getArrayRef(); + qcQubits = state.regionQubitMap[owner].getArrayRef(); } SmallVector qcoTargets; qcoTargets.reserve(registers.size() + qcQubits.size()); - llvm::append_range(qcoTargets, - resolveMappedTensors(state, anchor, registers)); - llvm::append_range(qcoTargets, resolveMappedQubits(state, anchor, qcQubits)); + for (auto reg : registers) { + qcoTargets.push_back(lookupMappedTensor(state, anchor, reg)); + } + for (auto qubit : qcQubits) { + qcoTargets.push_back(lookupMappedQubit(state, anchor, qubit)); + } return qcoTargets; } @@ -491,6 +479,17 @@ static void commitQubits(LoweringState& state, Operation* anchor, } } + if (isa(operation) || + (isa(operation) && + isQubitMemrefType(operation->getResult(0).getType()))) { + auto function = operation->getParentOfType(); + if (!function || operation->getBlock() != &function.getBody().front()) { + operation->emitOpError( + "quantum allocations must be in a function's entry block"); + return WalkResult::interrupt(); + } + } + const bool isModifier = isa(operation); for (Region& region : operation->getRegions()) { for (Block& block : region) { @@ -902,6 +901,7 @@ struct ConvertFuncReturnOp final : LoweringPattern { } state.localReferenceRegisters.erase(funcRegion); state.qubitMap.erase(funcRegion); + state.tensorMap.erase(funcRegion); state.functionQubitArguments.erase(function); rewriter.replaceOpWithNewOp(op, returnValues); @@ -1185,34 +1185,6 @@ struct ConvertMemRefAllocaOp final : LoweringPattern { } }; -/// Record a stable register slot; materialize its qubit at each quantum use. -/// -/// @par Example: -/// ```mlir -/// %q = memref.load %memref[%c0] : memref<3x!qc.qubit> -/// ``` -/// Quantum operations extract and reinsert the referenced qubit locally. -struct ConvertMemRefLoadOp final : LoweringPattern { - using LoweringPattern::LoweringPattern; - - LogicalResult matchAndRewrite(memref::LoadOp op, - PatternRewriter& rewriter) const override { - if (!isa(op.getMemRefType().getElementType())) { - return failure(); - } - - auto& state = getState(); - const auto accessIt = state.registerAccesses.find(op.getResult()); - assert(accessIt != state.registerAccesses.end() && - "qubit load provenance not found"); - accessIt->second.index = op.getIndices().front(); - - // Source reference retained until all QC users are lowered. - - return success(); - } -}; - /// A valid register store preserves the reference already in that slot. struct ConvertMemRefStoreOp final : LoweringPattern { using LoweringPattern::LoweringPattern; @@ -1572,6 +1544,7 @@ struct ConvertQCYieldOp final : LoweringPattern { auto targets = resolveMappedQubits(state, operation, frame->second); rewriter.replaceOpWithNewOp(op, targets); state.qubitMap.erase(region); + state.tensorMap.erase(region); state.modifierRegionQubits.erase(frame); return success(); } @@ -1636,12 +1609,11 @@ struct ConvertSCFForOp final : LoweringPattern { rewriter.replaceAllUsesWith(oldArgument, newArgument); } - SmallVector qubits(qubitMap.begin(), qubitMap.end()); - SmallVector registers(registerMap.begin(), registerMap.end()); - state.structuredValues[newForOp] = { - .qubits = qubits, - .registers = registers, - }; + auto& [qubits, registers] = state.structuredValues[newForOp]; + qubits = qubitMap.takeVector(); + registers = registerMap.takeVector(); + state.regionQubitMap.erase(op); + state.regionRegisterMap.erase(op); seedRegionMappings(state, newForOp.getRegion(), qubits, registers, dstBlock.getArguments().take_back(numQubits), dstBlock.getArguments() @@ -1742,12 +1714,11 @@ struct ConvertSCFWhileOp final : LoweringPattern { rewriter.replaceAllUsesWith(oldArgument, newArgument); } - SmallVector qubits(qubitMap.begin(), qubitMap.end()); - SmallVector registers(registerMap.begin(), registerMap.end()); - state.structuredValues[newWhileOp] = { - .qubits = qubits, - .registers = registers, - }; + auto& [qubits, registers] = state.structuredValues[newWhileOp]; + qubits = qubitMap.takeVector(); + registers = registerMap.takeVector(); + state.regionQubitMap.erase(op); + state.regionRegisterMap.erase(op); seedRegionMappings(state, newWhileOp.getBefore(), qubits, registers, newBeforeBlock->getArguments().take_back(numQubits), newBeforeBlock->getArguments() @@ -1820,12 +1791,11 @@ struct ConvertSCFIfOp final : LoweringPattern { thenBlock->getOperations().splice( thenBlock->end(), op.getThenRegion().front().getOperations()); - SmallVector qubits(qubitMap.begin(), qubitMap.end()); - SmallVector registers(registerMap.begin(), registerMap.end()); - state.structuredValues[newIfOp] = { - .qubits = qubits, - .registers = registers, - }; + auto& [qubits, registers] = state.structuredValues[newIfOp]; + qubits = qubitMap.takeVector(); + registers = registerMap.takeVector(); + state.regionQubitMap.erase(op); + state.regionRegisterMap.erase(op); if (!op.getElseRegion().empty()) { elseBlock->getOperations().splice( @@ -1898,9 +1868,11 @@ struct ConvertSCFIndexSwitchOp final : LoweringPattern { assignMappedQubits(state, op.getOperation(), qubitMap, newOp.getLinearResults().take_back(numQubits)); - SmallVector qubits(qubitMap.begin(), qubitMap.end()); - SmallVector registers(registerMap.begin(), registerMap.end()); - state.structuredValues[newOp] = {.qubits = qubits, .registers = registers}; + auto& [qubits, registers] = state.structuredValues[newOp]; + qubits = qubitMap.takeVector(); + registers = registerMap.takeVector(); + state.regionQubitMap.erase(op); + state.regionRegisterMap.erase(op); const auto newCaseRegions = newOp.getCaseRegions(); const auto oldCaseRegions = op.getCaseRegions(); @@ -1949,6 +1921,8 @@ struct ConvertSCFYieldOp final : LoweringPattern { SmallVector targets(op.getResults()); llvm::append_range(targets, resolveAllValues(state, operation)); + state.qubitMap.erase(op->getParentRegion()); + state.tensorMap.erase(op->getParentRegion()); if (isa(op->getParentOp())) { rewriter.replaceOpWithNewOp(op, targets); @@ -1981,6 +1955,8 @@ struct ConvertSCFConditionOp final : LoweringPattern { SmallVector targets(op.getArgs()); llvm::append_range(targets, resolveAllValues(state, operation)); + state.qubitMap.erase(op->getParentRegion()); + state.tensorMap.erase(op->getParentRegion()); rewriter.replaceOpWithNewOp(op, op.getCondition(), targets); @@ -1996,47 +1972,36 @@ struct ConvertSCFConditionOp final : LoweringPattern { /// Lower terminators after preceding operations so they yield the final mapped /// values. static LogicalResult lowerToQCO(ModuleOp moduleOp) { - LoweringState preflight; + LoweringState state; if (failed(validateSupportedInput(moduleOp)) || - failed(collectRegisterAccesses(moduleOp, preflight))) { + failed(collectRegisterAccesses(moduleOp, state))) { return failure(); } // Register indices still need normalization for alias validation. const bool needsNormalization = - !preflight.registerIds.empty() || !staticsAlreadyNormalized(moduleOp); - if (needsNormalization && failed(normalizeStaticQubits(moduleOp))) { - return failure(); - } - LoweringState normalizedState; - auto& state = needsNormalization ? normalizedState : preflight; - if (needsNormalization && failed(collectRegisterAccesses(moduleOp, state))) { - return failure(); - } - SmallVector unitaryFunctions; - for (auto function : moduleOp.getOps()) { - if (mqt::isUnitaryFunction(function)) { - unitaryFunctions.push_back(function); - function->removeAttr(mqt::MQTDialect::UnitaryAttrHelper::getNameStr()); + !state.registerIds.empty() || !staticsAlreadyNormalized(moduleOp); + if (needsNormalization) { + state = LoweringState(); + if (failed(normalizeStaticQubits(moduleOp)) || + failed(collectRegisterAccesses(moduleOp, state))) { + return failure(); } } - llvm::scope_exit unitaryGuard([&] { - for (auto function : unitaryFunctions) { - mqt::setUnitaryFunction(function); - } - }); collectStructuredCaptures(moduleOp, state); auto* ctx = moduleOp.getContext(); + auto* qcDialect = ctx->getLoadedDialect(); + auto* memrefDialect = ctx->getLoadedDialect(); QCToQCOTypeConverter converter(ctx); RewritePatternSet patterns(ctx); - patterns - .add(converter, ctx, &state); + patterns.add< + ConvertQCDeallocOp, ConvertQCMeasureOp, ConvertQCResetOp, + ConvertQCUnitaryOp, ConvertQCBarrierOp, ConvertQCCtrlOp, ConvertQCInvOp, + ConvertQCPowOp, ConvertQCYieldOp, ConvertSCFForOp, ConvertSCFWhileOp, + ConvertSCFIfOp, ConvertSCFIndexSwitchOp, ConvertSCFYieldOp, + ConvertSCFConditionOp, ConvertFuncReturnOp, ConvertMemRefAllocOp, + ConvertMemRefAllocaOp, ConvertMemRefStoreOp, ConvertMemRefDeallocOp, + ConvertFuncOp, ConvertFuncCallOp, ConvertQCCallOp>(converter, ctx, + &state); patterns.add>( converter, ctx, &state); #define MQT_GATE(KEY, NAME, GETTER, TARGETS, PARAMS, SUFFIX, CTL_SUFFIX) \ @@ -2078,8 +2043,7 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { sources.push_back(ref); continue; } - bool required = - operation->getDialect() == ctx->getLoadedDialect(); + bool required = operation->getDialect() == qcDialect; if (auto function = dyn_cast(operation)) { required = !converter.isSignatureLegal(function.getFunctionType()); if (required && !function.isExternal()) { @@ -2102,14 +2066,21 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { state.localReferenceRegisters.contains(region) || (it != state.qubitMap.end() && !it->second.empty()); } - if (operation->getDialect() == - ctx->getLoadedDialect()) { + if (operation->getDialect() == memrefDialect) { required = llvm::any_of(operation->getOperandTypes(), isQubitMemrefType) || llvm::any_of(operation->getResultTypes(), isQubitMemrefType); if (required && isa(operation)) { sources.push_back(operation); + if (auto load = dyn_cast(operation)) { + // Earlier signature rewrites may have replaced the index value. + const auto access = state.registerAccesses.find(load.getResult()); + assert(access != state.registerAccesses.end() && + "qubit load provenance not found"); + access->second.index = load.getIndices().front(); + continue; + } } } if (isa( @@ -2138,11 +2109,7 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { } block->eraseArguments(0, count); } - bool remains = false; - moduleOp.walk([&](Operation* op) { - remains |= op->getDialect() == ctx->getLoadedDialect(); - }); - return success(!remains); + return success(); } namespace { diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index ad8328cb92..0259a1b7df 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -156,15 +156,16 @@ FailureOr> QCProgramBuilder::LoopBuilder::finish() { QCProgramBuilder::QCProgramBuilder(MLIRContext* context) : ImplicitLocOpBuilder( FileLineColLoc::get(context, "", 1, 1), context), - ctx(context), moduleOp_(ModuleOp::create(*this)) { - ctx->loadDialect(); + moduleOp_(ModuleOp::create(*this)) { + getContext()->loadDialect(); } void QCProgramBuilder::initialize() { initialize({getI64Type()}); } void QCProgramBuilder::initialize(TypeRange returnTypes) { + checkFinalized(); // Set insertion point to the module body - setInsertionPointToStart(cast(moduleOp_).getBody()); + setInsertionPointToStart(moduleOp_->getBody()); // Create main function as entry point auto funcType = getFunctionType({}, returnTypes); @@ -178,7 +179,8 @@ void QCProgramBuilder::initialize(TypeRange returnTypes) { } void QCProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = mqt::getEntryPoint(cast(moduleOp_)); + checkFinalized(); + auto mainFunc = mqt::getEntryPoint(*moduleOp_); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } @@ -191,7 +193,7 @@ func::FuncOp QCProgramBuilder::createFunction( const StringRef name, const TypeRange argumentTypes, const function_ref(ValueRange)> body) { checkFinalized(); - auto moduleOp = cast(moduleOp_); + auto moduleOp = *moduleOp_; auto mainFunc = mqt::getEntryPoint(moduleOp); if (!mainFunc) { llvm::reportFatalUsageError( @@ -259,7 +261,7 @@ func::FuncOp QCProgramBuilder::createUnitaryFunction( SmallVector QCProgramBuilder::call(func::FuncOp callee, ValueRange operands) { checkFinalized(); - if (callee->getParentOp() != moduleOp_ || + if (callee->getParentOp() != moduleOp_->getOperation() || callee.getArgumentTypes() != operands.getTypes()) { llvm::reportFatalUsageError( "Call operands must match a function in the current module"); @@ -360,10 +362,11 @@ Value QCProgramBuilder::allocQubitRegisterStorage(const int64_t size, if (size <= 0) { llvm::reportFatalUsageError("Size must be positive"); } - auto memrefType = MemRefType::get({size}, QubitType::get(ctx)); + auto memrefType = MemRefType::get({size}, QubitType::get(getContext())); auto alloc = memref::AllocOp::create(*this, memrefType); if (!name.empty()) { - ctx->getLoadedDialect() + getContext() + ->getLoadedDialect() ->getRegisterNameAttrHelper() .setAttr(alloc, getStringAttr(name)); } @@ -386,10 +389,11 @@ Value QCProgramBuilder::allocClassicalBitRegister( llvm::reportFatalUsageError("Size must be positive"); } - const auto type = cbit::RegisterType::get(ctx, size); + const auto type = cbit::RegisterType::get(getContext(), size); auto alloc = cbit::AllocOp::create(*this, type, initialization); if (!name.empty()) { - ctx->getLoadedDialect() + getContext() + ->getLoadedDialect() ->getRegisterNameAttrHelper() .setAttr(alloc, getStringAttr(name)); } @@ -944,7 +948,7 @@ QCProgramBuilder& QCProgramBuilder::dealloc(Value qubit) { //===----------------------------------------------------------------------===// void QCProgramBuilder::checkFinalized() const { - if (ctx == nullptr) { + if (!moduleOp_) { llvm::reportFatalUsageError("QCProgramBuilder instance has been finalized"); } } @@ -952,7 +956,7 @@ void QCProgramBuilder::checkFinalized() const { void QCProgramBuilder::ensureAllocationMode( const AllocationMode requestedMode) { if (requestedMode == AllocationMode::Dynamic) { - auto entryPoint = mqt::getEntryPoint(cast(moduleOp_)); + auto entryPoint = mqt::getEntryPoint(*moduleOp_); if (!entryPoint || entryPoint.getBody().empty() || getInsertionBlock() != &entryPoint.getBody().front()) { llvm::reportFatalUsageError( @@ -994,7 +998,7 @@ OwningOpRef QCProgramBuilder::finalize(ValueRange returnValues) { // Ensure that the entry-point function exists and the insertion point is // valid. auto* insertionBlock = getInsertionBlock(); - auto mainFunc = mqt::getEntryPoint(cast(moduleOp_)); + auto mainFunc = mqt::getEntryPoint(*moduleOp_); if (mainFunc == nullptr) { llvm::reportFatalUsageError("Could not find entry-point function"); } @@ -1004,6 +1008,11 @@ OwningOpRef QCProgramBuilder::finalize(ValueRange returnValues) { "Insertion point is not in entry block of main function"); } + for (Value result : returnValues) { + allocatedQubits.remove(result); + allocatedQregs.remove(result); + } + for (auto qubit : allocatedQubits) { DeallocOp::create(*this, qubit); } @@ -1017,11 +1026,8 @@ OwningOpRef QCProgramBuilder::finalize(ValueRange returnValues) { // Add return statement with the given return values to the main function func::ReturnOp::create(*this, returnValues); - // Invalidate context to prevent use-after-finalize - ctx = nullptr; - // Transfer ownership to the caller - return cast(moduleOp_); + return moduleOp_.release(); } OwningOpRef QCProgramBuilder::build( diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index b8344c9a17..84f001a49f 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -64,16 +64,18 @@ namespace mlir::qco { QCOProgramBuilder::QCOProgramBuilder(MLIRContext* context) : ImplicitLocOpBuilder( FileLineColLoc::get(context, "", 1, 1), context), - ctx(context), moduleOp_(ModuleOp::create(*this)) { - ctx->loadDialect(); + moduleOp_(ModuleOp::create(*this)) { + getContext() + ->loadDialect(); } void QCOProgramBuilder::initialize() { initialize({getI64Type()}); } void QCOProgramBuilder::initialize(TypeRange returnTypes) { + checkFinalized(); // Set insertion point to the module body - setInsertionPointToStart(cast(moduleOp_).getBody()); + setInsertionPointToStart(moduleOp_->getBody()); // Create main function as entry point auto funcType = getFunctionType({}, returnTypes); @@ -87,7 +89,8 @@ void QCOProgramBuilder::initialize(TypeRange returnTypes) { } void QCOProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = mqt::getEntryPoint(cast(moduleOp_)); + checkFinalized(); + auto mainFunc = mqt::getEntryPoint(*moduleOp_); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } @@ -105,7 +108,7 @@ func::FuncOp QCOProgramBuilder::createFunction( const StringRef name, const TypeRange argumentTypes, const function_ref(ValueRange)> body) { checkFinalized(); - auto moduleOp = cast(moduleOp_); + auto moduleOp = *moduleOp_; auto mainFunc = mqt::getEntryPoint(moduleOp); if (!mainFunc) { llvm::reportFatalUsageError( @@ -195,22 +198,13 @@ func::FuncOp QCOProgramBuilder::createUnitaryFunction( SmallVector QCOProgramBuilder::call(func::FuncOp callee, ValueRange operands) { checkFinalized(); - if (callee->getParentOp() != moduleOp_ || + if (callee->getParentOp() != moduleOp_->getOperation() || callee.getArgumentTypes() != operands.getTypes()) { llvm::reportFatalUsageError( "Call operands must match a function in the current module"); } auto quantumArguments = getQuantumArgumentIndices(operands.getTypes()); - SmallVector quantumOperands; - for (const auto argument : quantumArguments) { - quantumOperands.emplace_back(operands[argument]); - } - quantumOperands = prepareInitArgs(quantumOperands); - SmallVector updatedOperands(operands); - for (auto [argument, operand] : - llvm::zip_equal(quantumArguments, quantumOperands)) { - updatedOperands[argument] = operand; - } + auto updatedOperands = prepareInitArgs(operands); SmallVector results; if (mqt::isUnitaryFunction(callee)) { @@ -230,8 +224,9 @@ SmallVector QCOProgramBuilder::call(func::FuncOp callee, const auto firstQuantumResult = results.size() - quantumArguments.size(); for (auto [index, result] : llvm::enumerate(results)) { if (index >= firstQuantumResult) { - updateQubitValueTracking(quantumOperands[index - firstQuantumResult], - result); + updateQubitValueTracking( + updatedOperands[quantumArguments[index - firstQuantumResult]], + result); } else if (isa(result.getType())) { validQubits.insert(result); } else if (isQubitTensor(result.getType())) { @@ -300,7 +295,8 @@ QCOProgramBuilder::allocQubitRegister(const int64_t size, } auto qtensor = qtensorAlloc(size); if (!name.empty()) { - ctx->getLoadedDialect() + getContext() + ->getLoadedDialect() ->getRegisterNameAttrHelper() .setAttr(qtensor.getDefiningOp(), getStringAttr(name)); } @@ -325,10 +321,11 @@ Value QCOProgramBuilder::allocClassicalBitRegister( llvm::reportFatalUsageError("Size must be positive"); } - const auto type = cbit::RegisterType::get(ctx, size); + const auto type = cbit::RegisterType::get(getContext(), size); auto alloc = cbit::AllocOp::create(*this, type, initialization); if (!name.empty()) { - ctx->getLoadedDialect() + getContext() + ->getLoadedDialect() ->getRegisterNameAttrHelper() .setAttr(alloc, getStringAttr(name)); } @@ -355,7 +352,7 @@ void QCOProgramBuilder::storeClassicalBit( // Linear Ownership Tracking Helpers //===----------------------------------------------------------------------===// -void QCOProgramBuilder::validateQubitValue(Value qubit) const { +void QCOProgramBuilder::validateQubitValue(Value qubit, bool checkScope) const { if (!validQubits.contains(qubit)) { llvm::errs() << "Attempting to use an invalid qubit SSA value. " << "The value may have been consumed by a previous operation " @@ -363,12 +360,17 @@ void QCOProgramBuilder::validateQubitValue(Value qubit) const { llvm::reportFatalUsageError( "Invalid qubit value used (either consumed or not tracked)"); } + if (checkScope && + qubit.getParentRegion() != getInsertionBlock()->getParent()) { + llvm::reportFatalUsageError( + "Quantum values must be passed explicitly through region arguments"); + } } -void QCOProgramBuilder::updateQubitTracking(Value inputQubit, - Value outputQubit) { +void QCOProgramBuilder::updateQubitTracking(Value inputQubit, Value outputQubit, + bool checkScope) { // Validate the input qubit - validateQubitValue(inputQubit); + validateQubitValue(inputQubit, checkScope); auto it = validQubits.find(inputQubit); auto trackedQubit = *it; @@ -381,7 +383,8 @@ void QCOProgramBuilder::updateQubitTracking(Value inputQubit, Qubit{outputQubit, trackedQubit.regId, trackedQubit.regIndex}); } -void QCOProgramBuilder::validateTensorValue(Value tensor) const { +void QCOProgramBuilder::validateTensorValue(Value tensor, + bool checkScope) const { if (!validTensors.contains(tensor)) { llvm::errs() << "Attempting to use an invalid tensor SSA value. " << "The value may have been consumed by a previous operation " @@ -389,6 +392,11 @@ void QCOProgramBuilder::validateTensorValue(Value tensor) const { llvm::reportFatalUsageError( "Invalid tensor value used (either consumed or not tracked)"); } + if (checkScope && + tensor.getParentRegion() != getInsertionBlock()->getParent()) { + llvm::reportFatalUsageError( + "Quantum values must be passed explicitly through region arguments"); + } auto tensorType = dyn_cast(tensor.getType()); if (!tensorType || tensorType.getRank() != 1) { @@ -400,9 +408,10 @@ void QCOProgramBuilder::validateTensorValue(Value tensor) const { } void QCOProgramBuilder::updateTensorTracking(Value inputTensor, - Value outputTensor) { + Value outputTensor, + bool checkScope) { // Validate the input tensor - validateTensorValue(inputTensor); + validateTensorValue(inputTensor, checkScope); auto it = validTensors.find(inputTensor); auto trackedTensor = *it; @@ -438,26 +447,28 @@ static bool isDefinedBefore(Value lhs, Value rhs) { Value QCOProgramBuilder::insertExtractedQubits(Value tensor, MutableArrayRef qubits) { + validateTensorValue(tensor); + auto inputTensor = tensor; llvm::sort(qubits, [](const Qubit& lhs, const Qubit& rhs) { return isDefinedBefore(lhs, rhs); }); for (const auto& qubit : qubits) { - auto newTensor = - qtensor::InsertOp::create(*this, qubit, tensor, qubit.regIndex) - .getResult(); - updateTensorTracking(tensor, newTensor); - tensor = newTensor; + validateQubitValue(qubit); + tensor = qtensor::InsertOp::create(*this, qubit, tensor, qubit.regIndex) + .getResult(); validQubits.erase(qubit); } + if (tensor != inputTensor) { + updateTensorTracking(inputTensor, tensor); + } return tensor; } SmallVector QCOProgramBuilder::prepareInitArgs(ValueRange initArgs) { - DenseSet initQubits; DenseMap> qubitsByRegister; for (auto initArg : initArgs) { if (isa(initArg.getType())) { - initQubits.insert(initArg); + validateQubitValue(initArg); } else if (isQubitTensor(initArg.getType())) { validateTensorValue(initArg); if (!qubitsByRegister.try_emplace(validTensors.find(initArg)->regId) @@ -468,6 +479,12 @@ SmallVector QCOProgramBuilder::prepareInitArgs(ValueRange initArgs) { } } if (!qubitsByRegister.empty()) { + DenseSet initQubits; + for (auto initArg : initArgs) { + if (isa(initArg.getType())) { + initQubits.insert(initArg); + } + } for (const auto& qubit : validQubits) { auto it = qubitsByRegister.find(qubit.regId); if (it != qubitsByRegister.end()) { @@ -516,10 +533,11 @@ void QCOProgramBuilder::updateQubitValueTracking(Value oldValue, if (oldValue.getType() != newValue.getType()) { llvm::reportFatalUsageError("Result types must match input types"); } + // Explicit region arguments and results transfer ownership across scopes. if (isa(oldValue.getType())) { - updateQubitTracking(oldValue, newValue); + updateQubitTracking(oldValue, newValue, false); } else if (isQubitTensor(oldValue.getType())) { - updateTensorTracking(oldValue, newValue); + updateTensorTracking(oldValue, newValue, false); } } @@ -609,12 +627,22 @@ Value QCOProgramBuilder::qtensorInsert( Value scalar, Value tensor, const std::variant& index) { checkFinalized(); + validateQubitValue(scalar); + validateTensorValue(tensor); auto indexValue = variantToValue(*this, getLoc(), index); + const auto& qubit = *validQubits.find(scalar); + if (qubit.regId != validTensors.find(tensor)->regId || !qubit.regIndex) { + llvm::reportFatalUsageError( + "Inserted qubit must belong to the destination register"); + } + auto originalIndex = getConstantIntValue(qubit.regIndex); + auto indexConstant = getConstantIntValue(indexValue); + if (originalIndex && indexConstant && originalIndex != indexConstant) { + llvm::reportFatalUsageError( + "Inserted qubit must return to its original slot"); + } auto insertOp = qtensor::InsertOp::create(*this, scalar, tensor, indexValue); - auto outTensor = insertOp.getResult(); - - validateQubitValue(scalar); validQubits.erase(scalar); updateTensorTracking(tensor, outTensor); @@ -1096,6 +1124,10 @@ QCOProgramBuilder::ctrl(ValueRange controls, ValueRange targets, function_ref(ValueRange)> body) { checkFinalized(); + for (Value qubit : controls) { + validateQubitValue(qubit); + } + auto ctrlOp = CtrlOp::create(*this, controls, targets); auto& block = ctrlOp.getBodyRegion().emplaceBlock(); auto qubitType = QubitType::get(getContext()); @@ -1115,7 +1147,7 @@ QCOProgramBuilder::ctrl(ValueRange controls, ValueRange targets, auto controlsOut = ctrlOp.getControlsOut(); for (auto [control, controlOut] : llvm::zip_equal(controls, controlsOut)) { - updateQubitTracking(control, controlOut); + updateQubitTracking(control, controlOut, false); } auto targetsOut = ctrlOp.getTargetsOut(); for (auto [target, targetOut] : @@ -1198,44 +1230,20 @@ QCOProgramBuilder::pow(const mqt::FloatParameter& exponent, ValueRange qubits, Value QCOProgramBuilder::pow(const mqt::FloatParameter& exponent, Value qubit, function_ref body) { - checkFinalized(); - - Value innerQubitOut; - auto powOp = - PowOp::create(*this, qubit, exponent, [&](Value qubitArg) -> Value { - updateQubitTracking(qubit, qubitArg); - innerQubitOut = body(qubitArg); - return innerQubitOut; - }); - - auto qubitsOut = powOp.getQubitsOut(); - assert(qubitsOut.size() == 1); - updateQubitTracking(innerQubitOut, qubitsOut.front()); - - return qubitsOut.front(); + return pow(exponent, ValueRange{qubit}, + [&](ValueRange args) { + return SmallVector{body(args.front())}; + }) + .front(); } std::pair QCOProgramBuilder::ctrl(ValueRange controls, Value target, function_ref body) { - checkFinalized(); - - Value innerTargetOut; - auto ctrlOp = - CtrlOp::create(*this, controls, target, [&](Value targetArg) -> Value { - updateQubitTracking(target, targetArg); - innerTargetOut = body(targetArg); - return innerTargetOut; + auto [controlsOut, targetsOut] = + ctrl(controls, ValueRange{target}, [&](ValueRange args) { + return SmallVector{body(args.front())}; }); - - auto controlsOut = ctrlOp.getControlsOut(); - for (auto [control, controlOut] : llvm::zip_equal(controls, controlsOut)) { - updateQubitTracking(control, controlOut); - } - auto targetsOut = ctrlOp.getTargetsOut(); - assert(targetsOut.size() == 1); - updateQubitTracking(innerTargetOut, targetsOut.front()); - return {controlsOut, targetsOut.front()}; } @@ -1248,20 +1256,11 @@ QCOProgramBuilder::ctrl(Value control, Value target, } Value QCOProgramBuilder::inv(Value qubit, function_ref body) { - checkFinalized(); - - Value innerQubitOut; - auto invOp = InvOp::create(*this, qubit, [&](Value qubitArg) -> Value { - updateQubitTracking(qubit, qubitArg); - innerQubitOut = body(qubitArg); - return innerQubitOut; - }); - - auto qubitsOut = invOp.getQubitsOut(); - assert(qubitsOut.size() == 1); - updateQubitTracking(innerQubitOut, qubitsOut.front()); - - return qubitsOut.front(); + return inv(ValueRange{qubit}, + [&](ValueRange args) { + return SmallVector{body(args.front())}; + }) + .front(); } //===----------------------------------------------------------------------===// @@ -1600,7 +1599,7 @@ QCOProgramBuilder::scfCondition(Value reg, //===----------------------------------------------------------------------===// void QCOProgramBuilder::checkFinalized() const { - if (ctx == nullptr) { + if (!moduleOp_) { llvm::reportFatalUsageError( "QCOProgramBuilder instance has been finalized"); } @@ -1609,7 +1608,7 @@ void QCOProgramBuilder::checkFinalized() const { void QCOProgramBuilder::ensureAllocationMode( const AllocationMode requestedMode) { if (requestedMode == AllocationMode::Dynamic) { - auto entryPoint = mqt::getEntryPoint(cast(moduleOp_)); + auto entryPoint = mqt::getEntryPoint(*moduleOp_); if (!entryPoint || entryPoint.getBody().empty() || getInsertionBlock() != &entryPoint.getBody().front()) { llvm::reportFatalUsageError( @@ -1661,6 +1660,7 @@ void QCOProgramBuilder::disposeLinearValues() { return isDefinedBefore(lhs, rhs); }); for (const auto& qubit : qubitsToSink) { + validateQubitValue(qubit); SinkOp::create(*this, qubit); } for (const auto& tensor : tensors) { @@ -1685,7 +1685,7 @@ OwningOpRef QCOProgramBuilder::finalize(ValueRange returnValues) { // Ensure that the entry-point function exists and the insertion point is // valid. auto* insertionBlock = getInsertionBlock(); - auto mainFunc = mqt::getEntryPoint(cast(moduleOp_)); + auto mainFunc = mqt::getEntryPoint(*moduleOp_); if (mainFunc == nullptr) { llvm::reportFatalUsageError("Could not find entry-point function"); } @@ -1713,10 +1713,7 @@ OwningOpRef QCOProgramBuilder::finalize(ValueRange returnValues) { // Add return statement with the given return values to the main function func::ReturnOp::create(*this, returnValues); - // Invalidate context to prevent use-after-finalize - ctx = nullptr; - - return cast(moduleOp_); + return moduleOp_.release(); } OwningOpRef QCOProgramBuilder::build( diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 7e0348cfbd..10e4e368ca 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -1823,15 +1823,15 @@ struct MappingPass : impl::MappingPassBase { DenseMap& cache) { SmallVector measurements; bool needsRouting = false; - WireIterator it(measurement.getQubitOut()); - for (; it != std::default_sentinel; ++it) { - Operation* op = it.operation(); + Operation* op = measurement; + while (true) { if (const auto cached = cache.find(op); cached != cache.end()) { needsRouting = cached->second; break; } if (auto next = dyn_cast(op)) { measurements.push_back(next); + op = *next.getQubitOut().user_begin(); continue; } // Validated tensor insertion tails become sinks during placement. diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index cb65a0bbca..4fc1630a42 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -16,7 +16,6 @@ #include "mqt/Dialect/QCO/Transforms/NativeSynthesis/SingleQubitFusion.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" -#include "mqt/Dialect/QCO/Utils/WireIterator.h" #include "mqt/Dialect/QTensor/IR/QTensorDialect.h" // IWYU pragma: keep (Passes.h.inc) #include "mlir/Dialect/Arith/IR/Arith.h" // IWYU pragma: keep (Passes.h.inc) @@ -81,11 +80,10 @@ scanFusableRun(UnitaryOpInterface head, const Matrix2x2& headMatrix, const decomposition::SingleQubitBasis basis, const CompilerTarget* target) { FusableRunScan scan; - for (auto* op : WireRange(head.getOutputQubit(0))) { - auto member = dyn_cast_or_null(op); - if (!member) { - break; - } + for (auto member = head; member; + member = dyn_cast( + *member.getOutputQubit(0).user_begin())) { + auto* op = member.getOperation(); const auto matrix = member.getOperation() == head.getOperation() ? std::optional{headMatrix} : getRunMemberMatrix(member); @@ -111,14 +109,13 @@ scanFusableRun(UnitaryOpInterface head, const Matrix2x2& headMatrix, static void eraseFusableRun(PatternRewriter& rewriter, UnitaryOpInterface head, UnitaryOpInterface tail) { // Tail-first: each erased op is dead once its successor is gone. - auto it = WireIterator(tail.getOutputQubit(0)); - auto* target = head.getOperation(); - while (*it != target) { - auto* current = *it; - --it; - rewriter.eraseOp(current); + while (tail != head) { + auto previous = + cast(tail.getInputQubit(0).getDefiningOp()); + rewriter.eraseOp(tail); + tail = previous; } - rewriter.eraseOp(target); + rewriter.eraseOp(head); } namespace { @@ -283,11 +280,12 @@ LogicalResult SingleQubitRunFusion::apply(Operation* operation) { return; } bool predecessorHasMatrix = false; - for (auto* current : WireRange(start.getOutputQubit(0))) { - auto op = dyn_cast(current); + for (auto op = start; op; op = dyn_cast( + *op.getOutputQubit(0).user_begin())) { if (!isRunMemberCandidate(op)) { break; } + auto* current = op.getOperation(); members.push_back(current); hasRuntimeParameters = hasRuntimeParameters || diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index 78ac9bdda6..b87a407b15 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -19,7 +19,6 @@ #include "mqt/Dialect/QCO/Transforms/NativeSynthesis/SingleQubitFusion.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" -#include "mqt/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Math/IR/Math.h" @@ -44,7 +43,6 @@ #include #include #include -#include #include #include #include @@ -505,14 +503,13 @@ struct MergeSingleQubitRotationGatesPattern final static SmallVector collectChain(UnitaryOpInterface start) { SmallVector chain{start}; - for (auto curr = std::next(WireIterator(start.getOutputQubit(0))); - curr != std::default_sentinel; ++curr) { - if (!isMergeable(curr.operation())) { - break; + while (true) { + auto* next = *chain.back().getOutputQubit(0).user_begin(); + if (!isMergeable(next)) { + return chain; } - chain.emplace_back(cast(*curr.operation())); + chain.emplace_back(cast(next)); } - return chain; } static bool hasDynamicParameter(ArrayRef chain) { diff --git a/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp b/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp index 198a62e98d..81e99292d2 100644 --- a/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp +++ b/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp @@ -128,8 +128,8 @@ FailureOr mlir::qco::traceQubitArgument(func::FuncOp function, } bool mlir::qco::hasCompleteTensorLifetime(Value tensor, unsigned depth) { - // Reject nesting at depth 64 to bound recursive stack use. - if (depth == 64) { + // Bound recursive stack use at nesting depth 64. + if (depth >= 64) { return false; } const auto isTensor = [](Value value) { @@ -236,7 +236,10 @@ LogicalResult mlir::qco::collectWireOrigins(Operation* root, }; auto result = root->walk([&](Operation* op, const WalkStage& stage) { if (auto unitary = dyn_cast(op)) { - tie(unitary.getInputQubits(), unitary.getOutputQubits()); + for (auto [input, output] : llvm::zip_equal(unitary.getInputQubits(), + unitary.getOutputQubits())) { + origins[output] = origin(input); + } return WalkResult::skip(); } if (!stage.isAfterAllRegions()) { @@ -303,10 +306,16 @@ LogicalResult mlir::qco::collectWireOrigins(Operation* root, } else if (auto insert = dyn_cast(op)) { origins[insert.getResult()] = origin(insert.getDest()); } else if (auto call = dyn_cast(op)) { - for (auto [index, result] : llvm::enumerate(call.getResults())) { - if (auto argument = qco::getCallArgumentForResult(call, index); - succeeded(argument)) { - origins[result] = origin(call.getOperand(*argument)); + auto arguments = getQuantumArgumentIndices(call.getOperandTypes()); + if (call.getNumResults() >= arguments.size() && + SymbolTable::lookupNearestSymbolFrom( + call, call.getCalleeAttr())) { + for (auto [argument, result] : llvm::zip_equal( + arguments, call.getResults().take_back(arguments.size()))) { + auto input = call.getOperand(argument); + if (result.getType() == input.getType()) { + origins[result] = origin(input); + } } } } @@ -336,7 +345,16 @@ LogicalResult mlir::qco::verifyLinearity(Operation* root) { } DenseSet staticIndices; + bool needsPositionalChecks = false; const auto walkResult = root->walk([&](Operation* op) { + if (!needsPositionalChecks) { + if (auto function = dyn_cast(op)) { + needsPositionalChecks = + !getQuantumArgumentIndices(function.getArgumentTypes()).empty(); + } else { + needsPositionalChecks = op->getNumRegions() != 0 && !isa(op); + } + } if (auto staticOp = dyn_cast(op)) { if (entryPoint && (entryPoint.isDeclaration() || @@ -374,6 +392,10 @@ LogicalResult mlir::qco::verifyLinearity(Operation* root) { if (walkResult.wasInterrupted()) { return failure(); } + // Flat owned circuits have no positional signatures to check. + if (!needsPositionalChecks) { + return success(); + } DenseMap origins; return collectWireOrigins(root, origins); } diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index fd98ae984b..c7bce4f2de 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -13,6 +13,7 @@ #include "mqt/Dialect/QCO/IR/QCODialect.h" #include "mqt/Dialect/QCO/IR/QCOInterfaces.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" +#include "mqt/Dialect/QCO/QCOUtils.h" #include "mqt/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/Func/IR/FuncOps.h" @@ -98,12 +99,9 @@ void WireIterator::forward() { qubit_ = op.getTiedLoopResult(qubit_.use_begin().getOperand()); }) .Case([&](scf::WhileOp op) { - // Because the scf::WhileOp doesn't implement "getLoopResults", we - // have to fallback to the following instead of using - // "getTiedLoopResult". - - OpOperand* operand = qubit_.use_begin().getOperand(); - qubit_ = op->getResult(operand->getOperandNumber()); + qubit_ = getCorrespondingQuantumValue( + op.getInits(), op.getResults(), + qubit_.use_begin()->getOperandNumber()); }) .Case([&](IfOp op) { qubit_ = op.getTiedResult(&(*qubit_.use_begin())); }) .Case([&](IndexSwitchOp op) { @@ -153,16 +151,9 @@ void WireIterator::backward() { llvm::reportFatalInternalError("expected result lookup"); }) .Case([&](scf::WhileOp op) { - // Because the scf::WhileOp doesn't implement "getLoopResults", we - // have to fallback to the following instead of using - // "getTiedLoopInit". - - if (auto result = dyn_cast(qubit_)) { - qubit_ = op.getInits()[result.getResultNumber()]; - return; - } - - llvm::reportFatalInternalError("expected result lookup"); + qubit_ = getCorrespondingQuantumValue( + op.getResults(), op.getInits(), + cast(qubit_).getResultNumber()); }) .Case([&](IfOp op) { if (auto result = dyn_cast(qubit_)) { diff --git a/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp b/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp index 5a201f55ce..e43d56e290 100644 --- a/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp +++ b/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp @@ -12,6 +12,7 @@ #include "mqt/Dialect/QCO/IR/QCODialect.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" +#include "mqt/Dialect/QCO/QCOUtils.h" #include "mqt/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/Func/IR/FuncOps.h" @@ -26,7 +27,6 @@ #include "llvm/Support/ErrorHandling.h" #include -#include #include namespace mlir::qtensor { @@ -41,51 +41,6 @@ TypedValue TensorIterator::tensor() const { return tensor_; } -[[nodiscard]] static TypedValue -whileResultForInit(scf::WhileOp op, OpOperand& init) { - auto current = cast>( - op.getBeforeBody()->getArgument(init.getOperandNumber())); - TensorIterator iterator(current); - while (true) { - assert(current.hasOneUse() && "expected linear semantics"); - auto* user = *current.user_begin(); - if (auto condition = dyn_cast(user)) { - const auto result = llvm::find(condition.getArgs(), current); - if (result == condition.getArgs().end()) { - llvm::reportFatalInternalError( - "expected scf.while tensor in condition arguments"); - } - const auto resultNumber = static_cast( - std::distance(condition.getArgs().begin(), result)); - return cast>(op.getResult(resultNumber)); - } - ++iterator; - if (iterator == std::default_sentinel) { - llvm::reportFatalInternalError( - "expected scf.while tensor to reach its condition"); - } - current = iterator.tensor(); - } -} - -[[nodiscard]] static TypedValue -whileInitForResult(scf::WhileOp op, OpResult result) { - auto condition = cast(op.getBeforeBody()->getTerminator()); - auto current = cast>( - condition.getArgs()[result.getResultNumber()]); - TensorIterator iterator(current); - while (iterator.operation() != nullptr) { - --iterator; - } - auto argument = dyn_cast(iterator.tensor()); - if (!argument || argument.getOwner() != op.getBeforeBody()) { - llvm::reportFatalInternalError( - "expected scf.while tensor to originate from a before-region argument"); - } - return cast>( - op.getInits()[argument.getArgNumber()]); -} - void TensorIterator::forward() { // If the iterator is a sentinel already, there is nothing to do. if (isSentinel_) { @@ -121,7 +76,10 @@ void TensorIterator::forward() { op.getTiedLoopResult(&*tensor_.use_begin())); }) .Case([&](scf::WhileOp op) { - tensor_ = whileResultForInit(op, *tensor_.use_begin().getOperand()); + tensor_ = cast>( + qco::getCorrespondingQuantumValue( + op.getInits(), op.getResults(), + tensor_.use_begin()->getOperandNumber())); }) .Case([&](qco::IfOp op) { tensor_ = cast>( @@ -188,13 +146,10 @@ void TensorIterator::backward() { "expected scf.for result for tied init lookup"); }) .Case([&](scf::WhileOp op) { - if (auto result = dyn_cast(tensor_)) { - tensor_ = whileInitForResult(op, result); - return; - } - - llvm::reportFatalInternalError( - "expected scf.while result for tied init lookup"); + tensor_ = cast>( + qco::getCorrespondingQuantumValue( + op.getResults(), op.getInits(), + cast(tensor_).getResultNumber())); }) .Case([&](qco::IfOp op) { if (auto res = dyn_cast(tensor_)) { diff --git a/mlir/unittests/Conversion/ConversionTestCases.h b/mlir/unittests/Conversion/ConversionTestCases.h new file mode 100644 index 0000000000..9de176c9f5 --- /dev/null +++ b/mlir/unittests/Conversion/ConversionTestCases.h @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "TestCaseUtils.h" +#include "programs/qc_programs.h" +#include "programs/qco_programs.h" + +#include +#include + +namespace mqt::test { + +inline constexpr std::array COMMON_QC_QCO_CONVERSION_CASES{ + std::tuple("StaticQubits", MQT_NAMED_BUILDER(mlir::qc::staticQubits), + MQT_NAMED_BUILDER(mlir::qco::staticQubits), false), + std::tuple("StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + MQT_NAMED_BUILDER(mlir::qco::staticQubitsWithOps), false), + std::tuple("StaticQubitsWithParametricOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithParametricOps), + MQT_NAMED_BUILDER(mlir::qco::staticQubitsWithParametricOps), + false), + std::tuple("StaticQubitsWithTwoTargetOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithTwoTargetOps), + MQT_NAMED_BUILDER(mlir::qco::staticQubitsWithTwoTargetOps), + false), + std::tuple("StaticQubitsWithCtrl", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithCtrl), + MQT_NAMED_BUILDER(mlir::qco::staticQubitsWithCtrl), false), + std::tuple("StaticQubitsWithInv", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithInv), + MQT_NAMED_BUILDER(mlir::qco::staticQubitsWithInv), false), + std::tuple("CtrlPowSx", MQT_NAMED_BUILDER(mlir::qc::ctrlPowSx), + MQT_NAMED_BUILDER(mlir::qco::ctrlPowSx), false), + std::tuple("PowTwo", MQT_NAMED_BUILDER(mlir::qc::powTwo), + MQT_NAMED_BUILDER(mlir::qco::powTwo), false), + std::tuple("CtrlTwo", MQT_NAMED_BUILDER(mlir::qc::ctrlTwo), + MQT_NAMED_BUILDER(mlir::qco::ctrlTwo), false), + std::tuple("CtrlTwoMixed", MQT_NAMED_BUILDER(mlir::qc::ctrlTwoMixed), + MQT_NAMED_BUILDER(mlir::qco::ctrlTwoMixed), false), + std::tuple("CtrlInvTwo", MQT_NAMED_BUILDER(mlir::qc::ctrlInvTwo), + MQT_NAMED_BUILDER(mlir::qco::ctrlInvTwo), false), + std::tuple("InverseiSWAP", MQT_NAMED_BUILDER(mlir::qc::inverseIswap), + MQT_NAMED_BUILDER(mlir::qco::inverseIswap), false), + std::tuple("InverseMultipleControllediSWAP", + MQT_NAMED_BUILDER(mlir::qc::inverseMultipleControlledIswap), + MQT_NAMED_BUILDER(mlir::qco::inverseMultipleControlledIswap), + false), + std::tuple("InvTwo", MQT_NAMED_BUILDER(mlir::qc::invTwo), + MQT_NAMED_BUILDER(mlir::qco::invTwo), false), + std::tuple("Barrier", MQT_NAMED_BUILDER(mlir::qc::barrier), + MQT_NAMED_BUILDER(mlir::qco::barrier), false), + std::tuple("BarrierTwoQubits", + MQT_NAMED_BUILDER(mlir::qc::barrierTwoQubits), + MQT_NAMED_BUILDER(mlir::qco::barrierTwoQubits), false), + std::tuple("BarrierMultipleQubits", + MQT_NAMED_BUILDER(mlir::qc::barrierMultipleQubits), + MQT_NAMED_BUILDER(mlir::qco::barrierMultipleQubits), false), + std::tuple("DCX", MQT_NAMED_BUILDER(mlir::qc::dcx), + MQT_NAMED_BUILDER(mlir::qco::dcx), false), + std::tuple("SingleControlledDCX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledDcx), + MQT_NAMED_BUILDER(mlir::qco::singleControlledDcx), false), + std::tuple("MultipleControlledDCX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledDcx), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledDcx), false), + std::tuple("ECR", MQT_NAMED_BUILDER(mlir::qc::ecr), + MQT_NAMED_BUILDER(mlir::qco::ecr), false), + std::tuple("SingleControlledECR", + MQT_NAMED_BUILDER(mlir::qc::singleControlledEcr), + MQT_NAMED_BUILDER(mlir::qco::singleControlledEcr), false), + std::tuple("MultipleControlledECR", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledEcr), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledEcr), false), + std::tuple("GlobalPhase", MQT_NAMED_BUILDER(mlir::qc::globalPhase), + MQT_NAMED_BUILDER(mlir::qco::globalPhase), false), + std::tuple("H", MQT_NAMED_BUILDER(mlir::qc::h), + MQT_NAMED_BUILDER(mlir::qco::h), false), + std::tuple("SingleControlledH", + MQT_NAMED_BUILDER(mlir::qc::singleControlledH), + MQT_NAMED_BUILDER(mlir::qco::singleControlledH), false), + std::tuple("MultipleControlledH", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledH), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledH), false), + std::tuple("HWithoutRegister", + MQT_NAMED_BUILDER(mlir::qc::hWithoutRegister), + MQT_NAMED_BUILDER(mlir::qco::hWithoutRegister), false), + std::tuple("iSWAP", MQT_NAMED_BUILDER(mlir::qc::iswap), + MQT_NAMED_BUILDER(mlir::qco::iswap), false), + std::tuple("SingleControllediSWAP", + MQT_NAMED_BUILDER(mlir::qc::singleControlledIswap), + MQT_NAMED_BUILDER(mlir::qco::singleControlledIswap), false), + std::tuple("MultipleControllediSWAP", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledIswap), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledIswap), false), + std::tuple("P", MQT_NAMED_BUILDER(mlir::qc::p), + MQT_NAMED_BUILDER(mlir::qco::p), false), + std::tuple("SingleControlledP", + MQT_NAMED_BUILDER(mlir::qc::singleControlledP), + MQT_NAMED_BUILDER(mlir::qco::singleControlledP), false), + std::tuple("MultipleControlledP", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledP), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledP), false), + std::tuple("RCCX", MQT_NAMED_BUILDER(mlir::qc::rccx), + MQT_NAMED_BUILDER(mlir::qco::rccx), false), + std::tuple("SingleControlledRCCX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRccx), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRccx), false), + std::tuple("MultipleControlledRCCX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRccx), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRccx), false), + std::tuple("R", MQT_NAMED_BUILDER(mlir::qc::r), + MQT_NAMED_BUILDER(mlir::qco::r), false), + std::tuple("SingleControlledR", + MQT_NAMED_BUILDER(mlir::qc::singleControlledR), + MQT_NAMED_BUILDER(mlir::qco::singleControlledR), false), + std::tuple("MultipleControlledR", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledR), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledR), false), + std::tuple("RX", MQT_NAMED_BUILDER(mlir::qc::rx), + MQT_NAMED_BUILDER(mlir::qco::rx), false), + std::tuple("SingleControlledRX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRx), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRx), false), + std::tuple("MultipleControlledRX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRx), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRx), false), + std::tuple("RXX", MQT_NAMED_BUILDER(mlir::qc::rxx), + MQT_NAMED_BUILDER(mlir::qco::rxx), false), + std::tuple("SingleControlledRXX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRxx), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRxx), false), + std::tuple("MultipleControlledRXX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRxx), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRxx), false), + std::tuple("RY", MQT_NAMED_BUILDER(mlir::qc::ry), + MQT_NAMED_BUILDER(mlir::qco::ry), false), + std::tuple("SingleControlledRY", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRy), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRy), false), + std::tuple("MultipleControlledRY", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRy), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRy), false), + std::tuple("RYY", MQT_NAMED_BUILDER(mlir::qc::ryy), + MQT_NAMED_BUILDER(mlir::qco::ryy), false), + std::tuple("SingleControlledRYY", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRyy), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRyy), false), + std::tuple("MultipleControlledRYY", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRyy), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRyy), false), + std::tuple("RZ", MQT_NAMED_BUILDER(mlir::qc::rz), + MQT_NAMED_BUILDER(mlir::qco::rz), false), + std::tuple("SingleControlledRZ", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRz), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRz), false), + std::tuple("MultipleControlledRZ", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRz), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRz), false), + std::tuple("RZX", MQT_NAMED_BUILDER(mlir::qc::rzx), + MQT_NAMED_BUILDER(mlir::qco::rzx), false), + std::tuple("SingleControlledRZX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRzx), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRzx), false), + std::tuple("MultipleControlledRZX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRzx), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRzx), false), + std::tuple("RZZ", MQT_NAMED_BUILDER(mlir::qc::rzz), + MQT_NAMED_BUILDER(mlir::qco::rzz), false), + std::tuple("SingleControlledRZZ", + MQT_NAMED_BUILDER(mlir::qc::singleControlledRzz), + MQT_NAMED_BUILDER(mlir::qco::singleControlledRzz), false), + std::tuple("MultipleControlledRZZ", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRzz), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledRzz), false), + std::tuple("S", MQT_NAMED_BUILDER(mlir::qc::s), + MQT_NAMED_BUILDER(mlir::qco::s), false), + std::tuple("SingleControlledS", + MQT_NAMED_BUILDER(mlir::qc::singleControlledS), + MQT_NAMED_BUILDER(mlir::qco::singleControlledS), false), + std::tuple("MultipleControlledS", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledS), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledS), false), + std::tuple("Sdg", MQT_NAMED_BUILDER(mlir::qc::sdg), + MQT_NAMED_BUILDER(mlir::qco::sdg), false), + std::tuple("SingleControlledSdg", + MQT_NAMED_BUILDER(mlir::qc::singleControlledSdg), + MQT_NAMED_BUILDER(mlir::qco::singleControlledSdg), false), + std::tuple("MultipleControlledSdg", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledSdg), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledSdg), false), + std::tuple("SWAP", MQT_NAMED_BUILDER(mlir::qc::swap), + MQT_NAMED_BUILDER(mlir::qco::swap), false), + std::tuple("SingleControlledSWAP", + MQT_NAMED_BUILDER(mlir::qc::singleControlledSwap), + MQT_NAMED_BUILDER(mlir::qco::singleControlledSwap), false), + std::tuple("MultipleControlledSWAP", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledSwap), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledSwap), false), + std::tuple("SX", MQT_NAMED_BUILDER(mlir::qc::sx), + MQT_NAMED_BUILDER(mlir::qco::sx), false), + std::tuple("SingleControlledSX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledSx), + MQT_NAMED_BUILDER(mlir::qco::singleControlledSx), false), + std::tuple("MultipleControlledSX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledSx), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledSx), false), + std::tuple("SXdg", MQT_NAMED_BUILDER(mlir::qc::sxdg), + MQT_NAMED_BUILDER(mlir::qco::sxdg), false), + std::tuple("SingleControlledSXdg", + MQT_NAMED_BUILDER(mlir::qc::singleControlledSxdg), + MQT_NAMED_BUILDER(mlir::qco::singleControlledSxdg), false), + std::tuple("MultipleControlledSXdg", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledSxdg), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledSxdg), false), + std::tuple("T", MQT_NAMED_BUILDER(mlir::qc::t_), + MQT_NAMED_BUILDER(mlir::qco::t_), false), + std::tuple("SingleControlledT", + MQT_NAMED_BUILDER(mlir::qc::singleControlledT), + MQT_NAMED_BUILDER(mlir::qco::singleControlledT), false), + std::tuple("MultipleControlledT", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledT), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledT), false), + std::tuple("Tdg", MQT_NAMED_BUILDER(mlir::qc::tdg), + MQT_NAMED_BUILDER(mlir::qco::tdg), false), + std::tuple("SingleControlledTdg", + MQT_NAMED_BUILDER(mlir::qc::singleControlledTdg), + MQT_NAMED_BUILDER(mlir::qco::singleControlledTdg), false), + std::tuple("MultipleControlledTdg", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledTdg), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledTdg), false), + std::tuple("U2", MQT_NAMED_BUILDER(mlir::qc::u2), + MQT_NAMED_BUILDER(mlir::qco::u2), false), + std::tuple("SingleControlledU2", + MQT_NAMED_BUILDER(mlir::qc::singleControlledU2), + MQT_NAMED_BUILDER(mlir::qco::singleControlledU2), false), + std::tuple("MultipleControlledU2", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledU2), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledU2), false), + std::tuple("U", MQT_NAMED_BUILDER(mlir::qc::u), + MQT_NAMED_BUILDER(mlir::qco::u), false), + std::tuple("SingleControlledU", + MQT_NAMED_BUILDER(mlir::qc::singleControlledU), + MQT_NAMED_BUILDER(mlir::qco::singleControlledU), false), + std::tuple("MultipleControlledU", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledU), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledU), false), + std::tuple("X", MQT_NAMED_BUILDER(mlir::qc::x), + MQT_NAMED_BUILDER(mlir::qco::x), false), + std::tuple("SingleControlledX", + MQT_NAMED_BUILDER(mlir::qc::singleControlledX), + MQT_NAMED_BUILDER(mlir::qco::singleControlledX), false), + std::tuple("MultipleControlledX", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledX), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledX), false), + std::tuple("RepeatedControlledX", + MQT_NAMED_BUILDER(mlir::qc::repeatedControlledX), + MQT_NAMED_BUILDER(mlir::qco::repeatedControlledX), false), + std::tuple("XXMinusYY", MQT_NAMED_BUILDER(mlir::qc::xxMinusYY), + MQT_NAMED_BUILDER(mlir::qco::xxMinusYY), false), + std::tuple("SingleControlledXXMinusYY", + MQT_NAMED_BUILDER(mlir::qc::singleControlledXxMinusYY), + MQT_NAMED_BUILDER(mlir::qco::singleControlledXxMinusYY), false), + std::tuple("MultipleControlledXXMinusYY", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledXxMinusYY), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledXxMinusYY), + false), + std::tuple("XXPlusYY", MQT_NAMED_BUILDER(mlir::qc::xxPlusYY), + MQT_NAMED_BUILDER(mlir::qco::xxPlusYY), false), + std::tuple("SingleControlledXXPlusYY", + MQT_NAMED_BUILDER(mlir::qc::singleControlledXxPlusYY), + MQT_NAMED_BUILDER(mlir::qco::singleControlledXxPlusYY), false), + std::tuple("MultipleControlledXXPlusYY", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledXxPlusYY), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledXxPlusYY), false), + std::tuple("Y", MQT_NAMED_BUILDER(mlir::qc::y), + MQT_NAMED_BUILDER(mlir::qco::y), false), + std::tuple("SingleControlledY", + MQT_NAMED_BUILDER(mlir::qc::singleControlledY), + MQT_NAMED_BUILDER(mlir::qco::singleControlledY), false), + std::tuple("MultipleControlledY", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledY), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledY), false), + std::tuple("Z", MQT_NAMED_BUILDER(mlir::qc::z), + MQT_NAMED_BUILDER(mlir::qco::z), false), + std::tuple("SingleControlledZ", + MQT_NAMED_BUILDER(mlir::qc::singleControlledZ), + MQT_NAMED_BUILDER(mlir::qco::singleControlledZ), false), + std::tuple("MultipleControlledZ", + MQT_NAMED_BUILDER(mlir::qc::multipleControlledZ), + MQT_NAMED_BUILDER(mlir::qco::multipleControlledZ), false), + std::tuple("SingleMeasurementToSingleBit", + MQT_NAMED_BUILDER(mlir::qc::singleMeasurementToSingleBit), + MQT_NAMED_BUILDER(mlir::qco::singleMeasurementToSingleBit), + false), + std::tuple("RepeatedMeasurementToSameBit", + MQT_NAMED_BUILDER(mlir::qc::repeatedMeasurementToSameBit), + MQT_NAMED_BUILDER(mlir::qco::repeatedMeasurementToSameBit), + false), + std::tuple("RepeatedMeasurementToDifferentBits", + MQT_NAMED_BUILDER(mlir::qc::repeatedMeasurementToDifferentBits), + MQT_NAMED_BUILDER(mlir::qco::repeatedMeasurementToDifferentBits), + false), + std::tuple( + "MultipleClassicalRegistersAndMeasurements", + MQT_NAMED_BUILDER(mlir::qc::multipleClassicalRegistersAndMeasurements), + MQT_NAMED_BUILDER(mlir::qco::multipleClassicalRegistersAndMeasurements), + false), + std::tuple("PartialMeasurementToRegister", + MQT_NAMED_BUILDER(mlir::qc::partialMeasurementToRegister), + MQT_NAMED_BUILDER(mlir::qco::partialMeasurementToRegister), + false), + std::tuple("DynamicallyIndexedMeasurement", + MQT_NAMED_BUILDER(mlir::qc::dynamicallyIndexedMeasurement), + MQT_NAMED_BUILDER(mlir::qco::dynamicallyIndexedMeasurement), + true), + std::tuple("MeasurementWithoutRegisters", + MQT_NAMED_BUILDER(mlir::qc::measurementWithoutRegisters), + MQT_NAMED_BUILDER(mlir::qco::measurementWithoutRegisters), + false), + std::tuple("ResetQubitAfterSingleOp", + MQT_NAMED_BUILDER(mlir::qc::resetQubitAfterSingleOp), + MQT_NAMED_BUILDER(mlir::qco::resetQubitAfterSingleOp), false), + std::tuple("ResetMultipleQubitsAfterSingleOp", + MQT_NAMED_BUILDER(mlir::qc::resetMultipleQubitsAfterSingleOp), + MQT_NAMED_BUILDER(mlir::qco::resetMultipleQubitsAfterSingleOp), + false), + std::tuple("NestedIfOpForLoop", + MQT_NAMED_BUILDER(mlir::qc::nestedIfOpForLoop), + MQT_NAMED_BUILDER(mlir::qco::nestedIfOpForLoop), true), + std::tuple("SimpleWhile", MQT_NAMED_BUILDER(mlir::qc::simpleWhileReset), + MQT_NAMED_BUILDER(mlir::qco::simpleWhileReset), false), + std::tuple("SimpleDoWhile", MQT_NAMED_BUILDER(mlir::qc::simpleDoWhileReset), + MQT_NAMED_BUILDER(mlir::qco::simpleDoWhileReset), false), + std::tuple("SimpleForLoop", MQT_NAMED_BUILDER(mlir::qc::simpleForLoop), + MQT_NAMED_BUILDER(mlir::qco::simpleForLoop), true), + std::tuple("NestedForLoopIfOp", + MQT_NAMED_BUILDER(mlir::qc::nestedForLoopIfOp), + MQT_NAMED_BUILDER(mlir::qco::nestedForLoopIfOp), true), + std::tuple( + "NestedForLoopCtrlOpWithSeparateQubit", + MQT_NAMED_BUILDER(mlir::qc::nestedForLoopCtrlOpWithSeparateQubit), + MQT_NAMED_BUILDER(mlir::qco::nestedForLoopCtrlOpWithSeparateQubit), + true), +}; + +} // namespace mqt::test diff --git a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 6dc21d472e..abd29e5e7b 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -20,6 +20,7 @@ #include "mqt/Dialect/QTensor/IR/QTensorDialect.h" #include "mqt/Support/Passes.h" +#include "Conversion/ConversionTestCases.h" #include "Support/IRVerification.h" #include "TestCaseUtils.h" #include "qc_programs.h" @@ -984,506 +985,35 @@ TEST_P(QCOToQCTest, ProgramEquivalence) { areModulesEquivalentWithPermutations(program.get(), reference.get())); } -// QCOToQC/QubitManagement/QubitManagement.cpp +static SmallVector commonConversionCases() { + SmallVector cases; + for (const auto& testCase : ::mqt::test::COMMON_QC_QCO_CONVERSION_CASES) { + cases.push_back({ + .name = std::get<0>(testCase), + .programBuilder = std::get<2>(testCase), + .referenceBuilder = std::get<1>(testCase), + }); + } + return cases; +} + +INSTANTIATE_TEST_SUITE_P(CommonCases, QCOToQCTest, + testing::ValuesIn(commonConversionCases())); + INSTANTIATE_TEST_SUITE_P( - QCOQubitManagementTest, QCOToQCTest, + SpecialCases, QCOToQCTest, testing::Values( - QCOToQCTestCase{"StaticQubits", MQT_NAMED_BUILDER(qco::staticQubits), - MQT_NAMED_BUILDER(qc::staticQubits)}, - QCOToQCTestCase{"StaticQubitsWithOps", - MQT_NAMED_BUILDER(qco::staticQubitsWithOps), - MQT_NAMED_BUILDER(qc::staticQubitsWithOps)}, - QCOToQCTestCase{"StaticQubitsWithParametricOps", - MQT_NAMED_BUILDER(qco::staticQubitsWithParametricOps), - MQT_NAMED_BUILDER(qc::staticQubitsWithParametricOps)}, - QCOToQCTestCase{"StaticQubitsWithTwoTargetOps", - MQT_NAMED_BUILDER(qco::staticQubitsWithTwoTargetOps), - MQT_NAMED_BUILDER(qc::staticQubitsWithTwoTargetOps)}, - QCOToQCTestCase{"StaticQubitsWithCtrl", - MQT_NAMED_BUILDER(qco::staticQubitsWithCtrl), - MQT_NAMED_BUILDER(qc::staticQubitsWithCtrl)}, - QCOToQCTestCase{"StaticQubitsWithInv", - MQT_NAMED_BUILDER(qco::staticQubitsWithInv), - MQT_NAMED_BUILDER(qc::staticQubitsWithInv)}, QCOToQCTestCase{"AllocDeallocPair", MQT_NAMED_BUILDER(qco::allocSinkPair), - MQT_NAMED_BUILDER(qc::allocDeallocPair)})); - -// QCOToQC/Modifiers/PowOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOPowOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"CtrlPowSx", - MQT_NAMED_BUILDER(qco::ctrlPowSx), - MQT_NAMED_BUILDER(qc::ctrlPowSx)}, - QCOToQCTestCase{"PowTwo", MQT_NAMED_BUILDER(qco::powTwo), - MQT_NAMED_BUILDER(qc::powTwo)})); - -// QCOToQC/Modifiers/CtrlOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOCtrlOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"CtrlTwo", MQT_NAMED_BUILDER(qco::ctrlTwo), - MQT_NAMED_BUILDER(qc::ctrlTwo)}, - QCOToQCTestCase{"CtrlTwoMixed", - MQT_NAMED_BUILDER(qco::ctrlTwoMixed), - MQT_NAMED_BUILDER(qc::ctrlTwoMixed)}, - QCOToQCTestCase{"CtrlInvTwo", - MQT_NAMED_BUILDER(qco::ctrlInvTwo), - MQT_NAMED_BUILDER(qc::ctrlInvTwo)})); - -// QCOToQC/Modifiers/InvOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOInvOpTest, QCOToQCTest, - testing::Values( - // iSWAP cannot be inverted with current canonicalization - QCOToQCTestCase{"InverseiSWAP", MQT_NAMED_BUILDER(qco::inverseIswap), - MQT_NAMED_BUILDER(qc::inverseIswap)}, - QCOToQCTestCase{"InverseMultipleControllediSWAP", - MQT_NAMED_BUILDER(qco::inverseMultipleControlledIswap), - MQT_NAMED_BUILDER(qc::inverseMultipleControlledIswap)}, - // Inverse DCX is not canonicalized in QCO + MQT_NAMED_BUILDER(qc::allocDeallocPair)}, QCOToQCTestCase{"InverseDCX", MQT_NAMED_BUILDER(qco::inverseDcx), MQT_NAMED_BUILDER(qc::dcx)}, QCOToQCTestCase{"InverseMultipleControlledDCX", MQT_NAMED_BUILDER(qco::inverseMultipleControlledDcx), MQT_NAMED_BUILDER(qc::multipleControlledDcx)}, - QCOToQCTestCase{"InvTwo", MQT_NAMED_BUILDER(qco::invTwo), - MQT_NAMED_BUILDER(qc::invTwo)})); - -// QCOToQC/Operations/StandardGates/BarrierOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOBarrierOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"Barrier", MQT_NAMED_BUILDER(qco::barrier), - MQT_NAMED_BUILDER(qc::barrier)}, - QCOToQCTestCase{"BarrierTwoQubits", - MQT_NAMED_BUILDER(qco::barrierTwoQubits), - MQT_NAMED_BUILDER(qc::barrierTwoQubits)}, - QCOToQCTestCase{ - "BarrierMultipleQubits", - MQT_NAMED_BUILDER(qco::barrierMultipleQubits), - MQT_NAMED_BUILDER(qc::barrierMultipleQubits)})); - -// QCOToQC/Operations/StandardGates/DcxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCODCXOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"DCX", MQT_NAMED_BUILDER(qco::dcx), - MQT_NAMED_BUILDER(qc::dcx)}, - QCOToQCTestCase{"SingleControlledDCX", - MQT_NAMED_BUILDER(qco::singleControlledDcx), - MQT_NAMED_BUILDER(qc::singleControlledDcx)}, - QCOToQCTestCase{ - "MultipleControlledDCX", - MQT_NAMED_BUILDER(qco::multipleControlledDcx), - MQT_NAMED_BUILDER(qc::multipleControlledDcx)})); - -// QCOToQC/Operations/StandardGates/EcrOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOECROpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"ECR", MQT_NAMED_BUILDER(qco::ecr), - MQT_NAMED_BUILDER(qc::ecr)}, - QCOToQCTestCase{"SingleControlledECR", - MQT_NAMED_BUILDER(qco::singleControlledEcr), - MQT_NAMED_BUILDER(qc::singleControlledEcr)}, - QCOToQCTestCase{ - "MultipleControlledECR", - MQT_NAMED_BUILDER(qco::multipleControlledEcr), - MQT_NAMED_BUILDER(qc::multipleControlledEcr)})); - -// QCOToQC/Operations/StandardGates/GphaseOp.cpp -INSTANTIATE_TEST_SUITE_P(QCOGPhaseOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{ - "GlobalPhase", MQT_NAMED_BUILDER(qco::globalPhase), - MQT_NAMED_BUILDER(qc::globalPhase)})); - -// QCOToQC/Operations/StandardGates/HOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOHOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"H", MQT_NAMED_BUILDER(qco::h), - MQT_NAMED_BUILDER(qc::h)}, - QCOToQCTestCase{"SingleControlledH", - MQT_NAMED_BUILDER(qco::singleControlledH), - MQT_NAMED_BUILDER(qc::singleControlledH)}, - QCOToQCTestCase{"MultipleControlledH", - MQT_NAMED_BUILDER(qco::multipleControlledH), - MQT_NAMED_BUILDER(qc::multipleControlledH)}, - QCOToQCTestCase{"HWithoutRegister", - MQT_NAMED_BUILDER(qco::hWithoutRegister), - MQT_NAMED_BUILDER(qc::hWithoutRegister)})); - -// QCOToQC/Operations/StandardGates/IswapOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOiSWAPOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"iSWAP", MQT_NAMED_BUILDER(qco::iswap), - MQT_NAMED_BUILDER(qc::iswap)}, - QCOToQCTestCase{"SingleControllediSWAP", - MQT_NAMED_BUILDER(qco::singleControlledIswap), - MQT_NAMED_BUILDER(qc::singleControlledIswap)}, - QCOToQCTestCase{"MultipleControllediSWAP", - MQT_NAMED_BUILDER(qco::multipleControlledIswap), - MQT_NAMED_BUILDER(qc::multipleControlledIswap)})); - -// QCOToQC/Operations/StandardGates/POp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOPOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"P", MQT_NAMED_BUILDER(qco::p), - MQT_NAMED_BUILDER(qc::p)}, - QCOToQCTestCase{"SingleControlledP", - MQT_NAMED_BUILDER(qco::singleControlledP), - MQT_NAMED_BUILDER(qc::singleControlledP)}, - QCOToQCTestCase{ - "MultipleControlledP", - MQT_NAMED_BUILDER(qco::multipleControlledP), - MQT_NAMED_BUILDER(qc::multipleControlledP)})); - -// QCOToQC/Operations/StandardGates/RCCXOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORCCXOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"RCCX", MQT_NAMED_BUILDER(qco::rccx), - MQT_NAMED_BUILDER(qc::rccx)}, - QCOToQCTestCase{"SingleControlledRCCX", - MQT_NAMED_BUILDER(qco::singleControlledRccx), - MQT_NAMED_BUILDER(qc::singleControlledRccx)}, - QCOToQCTestCase{"MultipleControlledRCCX", - MQT_NAMED_BUILDER(qco::multipleControlledRccx), - MQT_NAMED_BUILDER(qc::multipleControlledRccx)})); - -// QCOToQC/Operations/StandardGates/ROp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOROpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"R", MQT_NAMED_BUILDER(qco::r), - MQT_NAMED_BUILDER(qc::r)}, - QCOToQCTestCase{"SingleControlledR", - MQT_NAMED_BUILDER(qco::singleControlledR), - MQT_NAMED_BUILDER(qc::singleControlledR)}, - QCOToQCTestCase{ - "MultipleControlledR", - MQT_NAMED_BUILDER(qco::multipleControlledR), - MQT_NAMED_BUILDER(qc::multipleControlledR)})); - -// QCOToQC/Operations/StandardGates/RxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORXOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RX", MQT_NAMED_BUILDER(qco::rx), - MQT_NAMED_BUILDER(qc::rx)}, - QCOToQCTestCase{"SingleControlledRX", - MQT_NAMED_BUILDER(qco::singleControlledRx), - MQT_NAMED_BUILDER(qc::singleControlledRx)}, - QCOToQCTestCase{ - "MultipleControlledRX", - MQT_NAMED_BUILDER(qco::multipleControlledRx), - MQT_NAMED_BUILDER(qc::multipleControlledRx)})); - -// QCOToQC/Operations/StandardGates/RxxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORXXOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RXX", MQT_NAMED_BUILDER(qco::rxx), - MQT_NAMED_BUILDER(qc::rxx)}, - QCOToQCTestCase{"SingleControlledRXX", - MQT_NAMED_BUILDER(qco::singleControlledRxx), - MQT_NAMED_BUILDER(qc::singleControlledRxx)}, - QCOToQCTestCase{ - "MultipleControlledRXX", - MQT_NAMED_BUILDER(qco::multipleControlledRxx), - MQT_NAMED_BUILDER(qc::multipleControlledRxx)})); - -// QCOToQC/Operations/StandardGates/RyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORYOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RY", MQT_NAMED_BUILDER(qco::ry), - MQT_NAMED_BUILDER(qc::ry)}, - QCOToQCTestCase{"SingleControlledRY", - MQT_NAMED_BUILDER(qco::singleControlledRy), - MQT_NAMED_BUILDER(qc::singleControlledRy)}, - QCOToQCTestCase{ - "MultipleControlledRY", - MQT_NAMED_BUILDER(qco::multipleControlledRy), - MQT_NAMED_BUILDER(qc::multipleControlledRy)})); - -// QCOToQC/Operations/StandardGates/RyyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORYYOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RYY", MQT_NAMED_BUILDER(qco::ryy), - MQT_NAMED_BUILDER(qc::ryy)}, - QCOToQCTestCase{"SingleControlledRYY", - MQT_NAMED_BUILDER(qco::singleControlledRyy), - MQT_NAMED_BUILDER(qc::singleControlledRyy)}, - QCOToQCTestCase{ - "MultipleControlledRYY", - MQT_NAMED_BUILDER(qco::multipleControlledRyy), - MQT_NAMED_BUILDER(qc::multipleControlledRyy)})); - -// QCOToQC/Operations/StandardGates/RzOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORZOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RZ", MQT_NAMED_BUILDER(qco::rz), - MQT_NAMED_BUILDER(qc::rz)}, - QCOToQCTestCase{"SingleControlledRZ", - MQT_NAMED_BUILDER(qco::singleControlledRz), - MQT_NAMED_BUILDER(qc::singleControlledRz)}, - QCOToQCTestCase{ - "MultipleControlledRZ", - MQT_NAMED_BUILDER(qco::multipleControlledRz), - MQT_NAMED_BUILDER(qc::multipleControlledRz)})); - -// QCOToQC/Operations/StandardGates/RzxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORZXOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RZX", MQT_NAMED_BUILDER(qco::rzx), - MQT_NAMED_BUILDER(qc::rzx)}, - QCOToQCTestCase{"SingleControlledRZX", - MQT_NAMED_BUILDER(qco::singleControlledRzx), - MQT_NAMED_BUILDER(qc::singleControlledRzx)}, - QCOToQCTestCase{ - "MultipleControlledRZX", - MQT_NAMED_BUILDER(qco::multipleControlledRzx), - MQT_NAMED_BUILDER(qc::multipleControlledRzx)})); - -// QCOToQC/Operations/StandardGates/RzzOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCORZZOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"RZZ", MQT_NAMED_BUILDER(qco::rzz), - MQT_NAMED_BUILDER(qc::rzz)}, - QCOToQCTestCase{"SingleControlledRZZ", - MQT_NAMED_BUILDER(qco::singleControlledRzz), - MQT_NAMED_BUILDER(qc::singleControlledRzz)}, - QCOToQCTestCase{ - "MultipleControlledRZZ", - MQT_NAMED_BUILDER(qco::multipleControlledRzz), - MQT_NAMED_BUILDER(qc::multipleControlledRzz)})); - -// QCOToQC/Operations/StandardGates/SOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOSOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"S", MQT_NAMED_BUILDER(qco::s), - MQT_NAMED_BUILDER(qc::s)}, - QCOToQCTestCase{"SingleControlledS", - MQT_NAMED_BUILDER(qco::singleControlledS), - MQT_NAMED_BUILDER(qc::singleControlledS)}, - QCOToQCTestCase{ - "MultipleControlledS", - MQT_NAMED_BUILDER(qco::multipleControlledS), - MQT_NAMED_BUILDER(qc::multipleControlledS)})); - -// QCOToQC/Operations/StandardGates/SdgOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOSdgOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"Sdg", MQT_NAMED_BUILDER(qco::sdg), - MQT_NAMED_BUILDER(qc::sdg)}, - QCOToQCTestCase{"SingleControlledSdg", - MQT_NAMED_BUILDER(qco::singleControlledSdg), - MQT_NAMED_BUILDER(qc::singleControlledSdg)}, - QCOToQCTestCase{ - "MultipleControlledSdg", - MQT_NAMED_BUILDER(qco::multipleControlledSdg), - MQT_NAMED_BUILDER(qc::multipleControlledSdg)})); - -// QCOToQC/Operations/StandardGates/SwapOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOSWAPOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"SWAP", MQT_NAMED_BUILDER(qco::swap), - MQT_NAMED_BUILDER(qc::swap)}, - QCOToQCTestCase{"SingleControlledSWAP", - MQT_NAMED_BUILDER(qco::singleControlledSwap), - MQT_NAMED_BUILDER(qc::singleControlledSwap)}, - QCOToQCTestCase{"MultipleControlledSWAP", - MQT_NAMED_BUILDER(qco::multipleControlledSwap), - MQT_NAMED_BUILDER(qc::multipleControlledSwap)})); - -// QCOToQC/Operations/StandardGates/SxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOSXOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"SX", MQT_NAMED_BUILDER(qco::sx), - MQT_NAMED_BUILDER(qc::sx)}, - QCOToQCTestCase{"SingleControlledSX", - MQT_NAMED_BUILDER(qco::singleControlledSx), - MQT_NAMED_BUILDER(qc::singleControlledSx)}, - QCOToQCTestCase{ - "MultipleControlledSX", - MQT_NAMED_BUILDER(qco::multipleControlledSx), - MQT_NAMED_BUILDER(qc::multipleControlledSx)})); - -// QCOToQC/Operations/StandardGates/SxdgOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOSXdgOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"SXdg", MQT_NAMED_BUILDER(qco::sxdg), - MQT_NAMED_BUILDER(qc::sxdg)}, - QCOToQCTestCase{"SingleControlledSXdg", - MQT_NAMED_BUILDER(qco::singleControlledSxdg), - MQT_NAMED_BUILDER(qc::singleControlledSxdg)}, - QCOToQCTestCase{"MultipleControlledSXdg", - MQT_NAMED_BUILDER(qco::multipleControlledSxdg), - MQT_NAMED_BUILDER(qc::multipleControlledSxdg)})); - -// QCOToQC/Operations/StandardGates/TOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOTOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"T", MQT_NAMED_BUILDER(qco::t_), - MQT_NAMED_BUILDER(qc::t_)}, - QCOToQCTestCase{"SingleControlledT", - MQT_NAMED_BUILDER(qco::singleControlledT), - MQT_NAMED_BUILDER(qc::singleControlledT)}, - QCOToQCTestCase{ - "MultipleControlledT", - MQT_NAMED_BUILDER(qco::multipleControlledT), - MQT_NAMED_BUILDER(qc::multipleControlledT)})); - -// QCOToQC/Operations/StandardGates/TdgOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOTdgOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"Tdg", MQT_NAMED_BUILDER(qco::tdg), - MQT_NAMED_BUILDER(qc::tdg)}, - QCOToQCTestCase{"SingleControlledTdg", - MQT_NAMED_BUILDER(qco::singleControlledTdg), - MQT_NAMED_BUILDER(qc::singleControlledTdg)}, - QCOToQCTestCase{ - "MultipleControlledTdg", - MQT_NAMED_BUILDER(qco::multipleControlledTdg), - MQT_NAMED_BUILDER(qc::multipleControlledTdg)})); - -// QCOToQC/Operations/StandardGates/U2Op.cpp -INSTANTIATE_TEST_SUITE_P( - QCOU2OpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"U2", MQT_NAMED_BUILDER(qco::u2), - MQT_NAMED_BUILDER(qc::u2)}, - QCOToQCTestCase{"SingleControlledU2", - MQT_NAMED_BUILDER(qco::singleControlledU2), - MQT_NAMED_BUILDER(qc::singleControlledU2)}, - QCOToQCTestCase{ - "MultipleControlledU2", - MQT_NAMED_BUILDER(qco::multipleControlledU2), - MQT_NAMED_BUILDER(qc::multipleControlledU2)})); - -// QCOToQC/Operations/StandardGates/UOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOUOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"U", MQT_NAMED_BUILDER(qco::u), - MQT_NAMED_BUILDER(qc::u)}, - QCOToQCTestCase{"SingleControlledU", - MQT_NAMED_BUILDER(qco::singleControlledU), - MQT_NAMED_BUILDER(qc::singleControlledU)}, - QCOToQCTestCase{ - "MultipleControlledU", - MQT_NAMED_BUILDER(qco::multipleControlledU), - MQT_NAMED_BUILDER(qc::multipleControlledU)})); - -// QCOToQC/Operations/StandardGates/XOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOXOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"X", MQT_NAMED_BUILDER(qco::x), - MQT_NAMED_BUILDER(qc::x)}, - QCOToQCTestCase{"SingleControlledX", - MQT_NAMED_BUILDER(qco::singleControlledX), - MQT_NAMED_BUILDER(qc::singleControlledX)}, - QCOToQCTestCase{"MultipleControlledX", - MQT_NAMED_BUILDER(qco::multipleControlledX), - MQT_NAMED_BUILDER(qc::multipleControlledX)}, - QCOToQCTestCase{ - "RepeatedControlledX", - MQT_NAMED_BUILDER(qco::repeatedControlledX), - MQT_NAMED_BUILDER(qc::repeatedControlledX)})); - -// QCOToQC/Operations/StandardGates/XxMinusYyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOXXMinusYYOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"XXMinusYY", MQT_NAMED_BUILDER(qco::xxMinusYY), - MQT_NAMED_BUILDER(qc::xxMinusYY)}, - QCOToQCTestCase{"SingleControlledXXMinusYY", - MQT_NAMED_BUILDER(qco::singleControlledXxMinusYY), - MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY)}, - QCOToQCTestCase{"MultipleControlledXXMinusYY", - MQT_NAMED_BUILDER(qco::multipleControlledXxMinusYY), - MQT_NAMED_BUILDER(qc::multipleControlledXxMinusYY)})); - -// QCOToQC/Operations/StandardGates/XxPlusYyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOXXPlusYYOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"XXPlusYY", MQT_NAMED_BUILDER(qco::xxPlusYY), - MQT_NAMED_BUILDER(qc::xxPlusYY)}, - QCOToQCTestCase{"SingleControlledXXPlusYY", - MQT_NAMED_BUILDER(qco::singleControlledXxPlusYY), - MQT_NAMED_BUILDER(qc::singleControlledXxPlusYY)}, - QCOToQCTestCase{"MultipleControlledXXPlusYY", - MQT_NAMED_BUILDER(qco::multipleControlledXxPlusYY), - MQT_NAMED_BUILDER(qc::multipleControlledXxPlusYY)})); - -// QCOToQC/Operations/StandardGates/YOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOYOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"Y", MQT_NAMED_BUILDER(qco::y), - MQT_NAMED_BUILDER(qc::y)}, - QCOToQCTestCase{"SingleControlledY", - MQT_NAMED_BUILDER(qco::singleControlledY), - MQT_NAMED_BUILDER(qc::singleControlledY)}, - QCOToQCTestCase{ - "MultipleControlledY", - MQT_NAMED_BUILDER(qco::multipleControlledY), - MQT_NAMED_BUILDER(qc::multipleControlledY)})); - -// QCOToQC/Operations/StandardGates/ZOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOZOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"Z", MQT_NAMED_BUILDER(qco::z), - MQT_NAMED_BUILDER(qc::z)}, - QCOToQCTestCase{"SingleControlledZ", - MQT_NAMED_BUILDER(qco::singleControlledZ), - MQT_NAMED_BUILDER(qc::singleControlledZ)}, - QCOToQCTestCase{ - "MultipleControlledZ", - MQT_NAMED_BUILDER(qco::multipleControlledZ), - MQT_NAMED_BUILDER(qc::multipleControlledZ)})); - -// QCOToQC/Operations/MeasureOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOMeasureOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"SingleMeasurementToSingleBit", - MQT_NAMED_BUILDER(qco::singleMeasurementToSingleBit), - MQT_NAMED_BUILDER(qc::singleMeasurementToSingleBit)}, - QCOToQCTestCase{"RepeatedMeasurementToSameBit", - MQT_NAMED_BUILDER(qco::repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(qc::repeatedMeasurementToSameBit)}, - QCOToQCTestCase{ - "RepeatedMeasurementToDifferentBits", - MQT_NAMED_BUILDER(qco::repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(qc::repeatedMeasurementToDifferentBits)}, - QCOToQCTestCase{ - "MultipleClassicalRegistersAndMeasurements", - MQT_NAMED_BUILDER(qco::multipleClassicalRegistersAndMeasurements), - MQT_NAMED_BUILDER(qc::multipleClassicalRegistersAndMeasurements)}, - QCOToQCTestCase{"PartialMeasurementToRegister", - MQT_NAMED_BUILDER(qco::partialMeasurementToRegister), - MQT_NAMED_BUILDER(qc::partialMeasurementToRegister)}, - QCOToQCTestCase{"DynamicallyIndexedMeasurement", - MQT_NAMED_BUILDER(qco::dynamicallyIndexedMeasurement), - MQT_NAMED_BUILDER(qc::dynamicallyIndexedMeasurement)}, - QCOToQCTestCase{"MeasurementWithoutRegisters", - MQT_NAMED_BUILDER(qco::measurementWithoutRegisters), - MQT_NAMED_BUILDER(qc::measurementWithoutRegisters)})); - -// QCOToQC/Operations/ResetOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOResetOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"ResetQubitAfterSingleOp", - MQT_NAMED_BUILDER(qco::resetQubitAfterSingleOp), - MQT_NAMED_BUILDER(qc::resetQubitAfterSingleOp)}, - QCOToQCTestCase{ - "ResetMultipleQubitsAfterSingleOp", - MQT_NAMED_BUILDER(qco::resetMultipleQubitsAfterSingleOp), - MQT_NAMED_BUILDER(qc::resetMultipleQubitsAfterSingleOp)}, QCOToQCTestCase{"RepeatedResetAfterSingleOp", MQT_NAMED_BUILDER(qco::repeatedResetAfterSingleOp), - MQT_NAMED_BUILDER(qc::resetQubitAfterSingleOp)})); - -// QCOToQC/Operations/IfOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOIfOpTest, QCOToQCTest, - testing::Values( + MQT_NAMED_BUILDER(qc::resetQubitAfterSingleOp)}, QCOToQCTestCase{"SimpleIfOp", MQT_NAMED_BUILDER(qco::simpleIf), MQT_NAMED_BUILDER(qc::simpleIf)}, QCOToQCTestCase{"IfElse", MQT_NAMED_BUILDER(qco::ifElse), @@ -1495,50 +1025,18 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(qc::ifWithMeasurement)}, QCOToQCTestCase{"IfWithCreg", MQT_NAMED_BUILDER(qco::ifWithCreg), MQT_NAMED_BUILDER(qc::ifWithCreg)}, - QCOToQCTestCase{"NestedIfOpForLoop", - MQT_NAMED_BUILDER(qco::nestedIfOpForLoop), - MQT_NAMED_BUILDER(qc::nestedIfOpForLoop)})); - -// QCOToQC/Operations/IndexSwitchOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOIndexSwitchOpTest, QCOToQCTest, - testing::Values(QCOToQCTestCase{"SimpleIndexSwitchOp", - MQT_NAMED_BUILDER(qco::simpleIndexSwitch), - MQT_NAMED_BUILDER(qc::simpleIndexSwitch)}, - QCOToQCTestCase{ - "IndexSwitchMultiCase", + QCOToQCTestCase{"SimpleIndexSwitchOp", + MQT_NAMED_BUILDER(qco::simpleIndexSwitch), + MQT_NAMED_BUILDER(qc::simpleIndexSwitch)}, + QCOToQCTestCase{"IndexSwitchMultiCase", MQT_NAMED_BUILDER(qco::indexSwitchMultiCase), - MQT_NAMED_BUILDER(qc::indexSwitchMultiCase)})); - -// QCOToQC/Operations/WhileOp.cpp -INSTANTIATE_TEST_SUITE_P( - SCFWhileOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"SimpleWhile", MQT_NAMED_BUILDER(qco::simpleWhileReset), - MQT_NAMED_BUILDER(qc::simpleWhileReset)}, - QCOToQCTestCase{"SimpleDoWhile", - MQT_NAMED_BUILDER(qco::simpleDoWhileReset), - MQT_NAMED_BUILDER(qc::simpleDoWhileReset)})); - -// QCOToQC/Operations/ForOp.cpp -INSTANTIATE_TEST_SUITE_P( - SCFForOpTest, QCOToQCTest, - testing::Values( - QCOToQCTestCase{"SimpleForLoop", MQT_NAMED_BUILDER(qco::simpleForLoop), - MQT_NAMED_BUILDER(qc::simpleForLoop)}, - QCOToQCTestCase{"NestedForLoopIfOp", - MQT_NAMED_BUILDER(qco::nestedForLoopIfOp), - MQT_NAMED_BUILDER(qc::nestedForLoopIfOp)}, + MQT_NAMED_BUILDER(qc::indexSwitchMultiCase)}, QCOToQCTestCase{"NestedForLoopWhileOp", MQT_NAMED_BUILDER(qco::nestedForLoopWhileOp), MQT_NAMED_BUILDER(qc::nestedForLoopWhileOp)}, QCOToQCTestCase{"NestedForLoopSwitchOp", MQT_NAMED_BUILDER(qco::nestedForLoopSwitchOp), MQT_NAMED_BUILDER(qc::nestedForLoopSwitchOp)}, - QCOToQCTestCase{ - "NestedForLoopCtrlOpWithSeparateQubit", - MQT_NAMED_BUILDER(qco::nestedForLoopCtrlOpWithSeparateQubit), - MQT_NAMED_BUILDER(qc::nestedForLoopCtrlOpWithSeparateQubit)}, QCOToQCTestCase{ "NestedForLoopCtrlOpWithExtractedQubit", MQT_NAMED_BUILDER(qco::nestedForLoopCtrlOpWithExtractedQubit), diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index baa4e46c91..e079d11e51 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -28,6 +28,7 @@ #include "mqt/Dialect/QTensor/IR/QTensorOps.h" #include "mqt/Support/Passes.h" +#include "Conversion/ConversionTestCases.h" #include "Support/IRVerification.h" #include "TestCaseUtils.h" #include "qc_programs.h" @@ -82,7 +83,6 @@ struct QCToQCOTestCase { ::mqt::test::NamedMLIRBuilder programBuilder; ::mqt::test::NamedMLIRBuilder referenceBuilder; bool expectsCompleteTensorState = false; - bool skipReferenceComparison = false; friend std::ostream& operator<<(std::ostream& os, const QCToQCOTestCase& info); @@ -1415,6 +1415,40 @@ module { } } +TEST_F(QCToQCORegressionTest, RejectsNestedQuantumAllocationsBeforeRewriting) { + for (const auto* allocation : { + "%q = qc.alloc : !qc.qubit\nqc.dealloc %q : !qc.qubit", + "%q = memref.alloc() : memref<1x!qc.qubit>\n" + "memref.dealloc %q : memref<1x!qc.qubit>", + "%q = memref.alloca() : memref<1x!qc.qubit>", + }) { + SCOPED_TRACE(allocation); + const auto source = std::string(R"mlir(module { + func.func @main(%condition: i1) { + %outer = qc.alloc : !qc.qubit + scf.if %condition { + )mlir") + allocation + + "\n} qc.dealloc %outer : !qc.qubit\nreturn } }"; + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + OwningOpRef original = moduleOp->clone(); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnosed |= + StringRef(diagnostic.str()) + .contains( + "quantum allocations must be in a function's entry block"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*moduleOp))); + EXPECT_TRUE(diagnosed); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + moduleOp->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); + } +} + TEST_F(QCToQCORegressionTest, CapturesQubitsUsedByPowInsideFor) { constexpr llvm::StringLiteral source = R"mlir( module { @@ -1602,8 +1636,8 @@ TEST_F(QCToQCORegressionTest, RejectsRegisterAliasingExposedByIndexFolding) { } TEST_P(QCToQCOTest, ProgramConversion) { - const auto& [_, programBuilder, referenceBuilder, expectsCompleteTensorState, - skipReferenceComparison] = GetParam(); + const auto& [_, programBuilder, referenceBuilder, + expectsCompleteTensorState] = GetParam(); const auto name = " (" + GetParam().name + ")"; ::mqt::test::DeferredPrinter printer; @@ -1629,524 +1663,48 @@ TEST_P(QCToQCOTest, ProgramConversion) { printer.record(program.get(), "Canonicalized Converted QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - if (!skipReferenceComparison) { - auto reference = - ::mqt::test::buildMLIRProgram(context.get(), referenceBuilder); - ASSERT_TRUE(reference); - printer.record(reference.get(), "Reference QCO IR" + name); - EXPECT_TRUE(verify(*reference).succeeded()); + auto reference = + ::mqt::test::buildMLIRProgram(context.get(), referenceBuilder); + ASSERT_TRUE(reference); + printer.record(reference.get(), "Reference QCO IR" + name); + EXPECT_TRUE(verify(*reference).succeeded()); - EXPECT_TRUE(runQCOCleanupPipeline(reference.get()).succeeded()); - printer.record(reference.get(), "Canonicalized Reference QCO IR" + name); - EXPECT_TRUE(verify(*reference).succeeded()); + EXPECT_TRUE(runQCOCleanupPipeline(reference.get()).succeeded()); + printer.record(reference.get(), "Canonicalized Reference QCO IR" + name); + EXPECT_TRUE(verify(*reference).succeeded()); - EXPECT_TRUE( - areModulesEquivalentWithPermutations(program.get(), reference.get())); + EXPECT_TRUE( + areModulesEquivalentWithPermutations(program.get(), reference.get())); +} + +static SmallVector commonConversionCases() { + SmallVector cases; + for (const auto& [name, qcBuilder, qcoBuilder, completeTensorState] : + ::mqt::test::COMMON_QC_QCO_CONVERSION_CASES) { + cases.push_back({ + .name = name, + .programBuilder = qcBuilder, + .referenceBuilder = qcoBuilder, + .expectsCompleteTensorState = completeTensorState, + }); } + return cases; } -// QCToQCO/QubitManagement/StaticOp.cpp +INSTANTIATE_TEST_SUITE_P(CommonCases, QCToQCOTest, + testing::ValuesIn(commonConversionCases())); + INSTANTIATE_TEST_SUITE_P( - QCStaticOpTest, QCToQCOTest, + SpecialCases, QCToQCOTest, testing::Values( - QCToQCOTestCase{"StaticQubits", MQT_NAMED_BUILDER(qc::staticQubits), - MQT_NAMED_BUILDER(qco::staticQubits)}, - QCToQCOTestCase{"StaticQubitsWithOps", - MQT_NAMED_BUILDER(qc::staticQubitsWithOps), - MQT_NAMED_BUILDER(qco::staticQubitsWithOps)}, - QCToQCOTestCase{"StaticQubitsWithParametricOps", - MQT_NAMED_BUILDER(qc::staticQubitsWithParametricOps), - MQT_NAMED_BUILDER(qco::staticQubitsWithParametricOps)}, - QCToQCOTestCase{"StaticQubitsWithTwoTargetOps", - MQT_NAMED_BUILDER(qc::staticQubitsWithTwoTargetOps), - MQT_NAMED_BUILDER(qco::staticQubitsWithTwoTargetOps)}, - QCToQCOTestCase{"StaticQubitsWithCtrl", - MQT_NAMED_BUILDER(qc::staticQubitsWithCtrl), - MQT_NAMED_BUILDER(qco::staticQubitsWithCtrl)}, - QCToQCOTestCase{"StaticQubitsWithInv", - MQT_NAMED_BUILDER(qc::staticQubitsWithInv), - MQT_NAMED_BUILDER(qco::staticQubitsWithInv)}, QCToQCOTestCase{"AllocDeallocPair", MQT_NAMED_BUILDER(qc::allocDeallocPair), - MQT_NAMED_BUILDER(qco::emptyQCO)})); - -// QCToQCO/Modifiers/PowOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCPowOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"CtrlPowSx", - MQT_NAMED_BUILDER(qc::ctrlPowSx), - MQT_NAMED_BUILDER(qco::ctrlPowSx)}, - QCToQCOTestCase{"PowTwo", MQT_NAMED_BUILDER(qc::powTwo), - MQT_NAMED_BUILDER(qco::powTwo)})); - -// QCToQCO/Modifiers/CtrlOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCCtrlOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"CtrlTwo", MQT_NAMED_BUILDER(qc::ctrlTwo), - MQT_NAMED_BUILDER(qco::ctrlTwo)}, - QCToQCOTestCase{"CtrlTwoMixed", - MQT_NAMED_BUILDER(qc::ctrlTwoMixed), - MQT_NAMED_BUILDER(qco::ctrlTwoMixed)}, - QCToQCOTestCase{"CtrlInvTwo", - MQT_NAMED_BUILDER(qc::ctrlInvTwo), - MQT_NAMED_BUILDER(qco::ctrlInvTwo)})); - -// QCToQCO/Modifiers/InvOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCInvOpTest, QCToQCOTest, - testing::Values( - // iSWAP cannot be inverted with current canonicalization - QCToQCOTestCase{"InverseiSWAP", MQT_NAMED_BUILDER(qc::inverseIswap), - MQT_NAMED_BUILDER(qco::inverseIswap)}, - QCToQCOTestCase{"InverseMultipleControllediSWAP", - MQT_NAMED_BUILDER(qc::inverseMultipleControlledIswap), - MQT_NAMED_BUILDER(qco::inverseMultipleControlledIswap)}, - QCToQCOTestCase{"InvTwo", MQT_NAMED_BUILDER(qc::invTwo), - MQT_NAMED_BUILDER(qco::invTwo)})); - -// QCToQCO/Operations/StandardGates/BarrierOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCBarrierOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"Barrier", MQT_NAMED_BUILDER(qc::barrier), - MQT_NAMED_BUILDER(qco::barrier)}, - QCToQCOTestCase{"BarrierTwoQubits", - MQT_NAMED_BUILDER(qc::barrierTwoQubits), - MQT_NAMED_BUILDER(qco::barrierTwoQubits)}, - QCToQCOTestCase{ - "BarrierMultipleQubits", - MQT_NAMED_BUILDER(qc::barrierMultipleQubits), - MQT_NAMED_BUILDER(qco::barrierMultipleQubits)})); - -// QCToQCO/Operations/StandardGates/DcxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCDCXOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"DCX", MQT_NAMED_BUILDER(qc::dcx), - MQT_NAMED_BUILDER(qco::dcx)}, - QCToQCOTestCase{"SingleControlledDCX", - MQT_NAMED_BUILDER(qc::singleControlledDcx), - MQT_NAMED_BUILDER(qco::singleControlledDcx)}, - QCToQCOTestCase{"MultipleControlledDCX", - MQT_NAMED_BUILDER(qc::multipleControlledDcx), - MQT_NAMED_BUILDER(qco::multipleControlledDcx)})); - -// QCToQCO/Operations/StandardGates/EcrOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCECROpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"ECR", MQT_NAMED_BUILDER(qc::ecr), - MQT_NAMED_BUILDER(qco::ecr)}, - QCToQCOTestCase{"SingleControlledECR", - MQT_NAMED_BUILDER(qc::singleControlledEcr), - MQT_NAMED_BUILDER(qco::singleControlledEcr)}, - QCToQCOTestCase{"MultipleControlledECR", - MQT_NAMED_BUILDER(qc::multipleControlledEcr), - MQT_NAMED_BUILDER(qco::multipleControlledEcr)})); - -// QCToQCO/Operations/StandardGates/GphaseOp.cpp -INSTANTIATE_TEST_SUITE_P(QCGPhaseOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{ - "GlobalPhase", MQT_NAMED_BUILDER(qc::globalPhase), - MQT_NAMED_BUILDER(qco::globalPhase)})); - -// QCToQCO/Operations/StandardGates/HOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCHOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"H", MQT_NAMED_BUILDER(qc::h), - MQT_NAMED_BUILDER(qco::h)}, - QCToQCOTestCase{"SingleControlledH", - MQT_NAMED_BUILDER(qc::singleControlledH), - MQT_NAMED_BUILDER(qco::singleControlledH)}, - QCToQCOTestCase{ - "MultipleControlledH", - MQT_NAMED_BUILDER(qc::multipleControlledH), - MQT_NAMED_BUILDER(qco::multipleControlledH)}, - QCToQCOTestCase{"HWithoutRegister", - MQT_NAMED_BUILDER(qc::hWithoutRegister), - MQT_NAMED_BUILDER(qco::hWithoutRegister)})); - -// QCToQCO/Operations/StandardGates/IdOp.cpp -INSTANTIATE_TEST_SUITE_P(QCIDOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{ - "Identity", MQT_NAMED_BUILDER(qc::identity), - MQT_NAMED_BUILDER(qco::alloc1QubitRegister)})); - -// QCToQCO/Operations/StandardGates/IswapOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCiSWAPOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"iSWAP", MQT_NAMED_BUILDER(qc::iswap), - MQT_NAMED_BUILDER(qco::iswap)}, - QCToQCOTestCase{"SingleControllediSWAP", - MQT_NAMED_BUILDER(qc::singleControlledIswap), - MQT_NAMED_BUILDER(qco::singleControlledIswap)}, - QCToQCOTestCase{"MultipleControllediSWAP", - MQT_NAMED_BUILDER(qc::multipleControlledIswap), - MQT_NAMED_BUILDER(qco::multipleControlledIswap)})); - -// QCToQCO/Operations/StandardGates/POp.cpp -INSTANTIATE_TEST_SUITE_P( - QCPOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"P", MQT_NAMED_BUILDER(qc::p), - MQT_NAMED_BUILDER(qco::p)}, - QCToQCOTestCase{"SingleControlledP", - MQT_NAMED_BUILDER(qc::singleControlledP), - MQT_NAMED_BUILDER(qco::singleControlledP)}, - QCToQCOTestCase{ - "MultipleControlledP", - MQT_NAMED_BUILDER(qc::multipleControlledP), - MQT_NAMED_BUILDER(qco::multipleControlledP)})); - -// QCToQCO/Operations/StandardGates/RCCXOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRCCXOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"RCCX", MQT_NAMED_BUILDER(qc::rccx), - MQT_NAMED_BUILDER(qco::rccx)}, - QCToQCOTestCase{"SingleControlledRCCX", - MQT_NAMED_BUILDER(qc::singleControlledRccx), - MQT_NAMED_BUILDER(qco::singleControlledRccx)}, - QCToQCOTestCase{"MultipleControlledRCCX", - MQT_NAMED_BUILDER(qc::multipleControlledRccx), - MQT_NAMED_BUILDER(qco::multipleControlledRccx)})); - -// QCToQCO/Operations/StandardGates/ROp.cpp -INSTANTIATE_TEST_SUITE_P( - QCROpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"R", MQT_NAMED_BUILDER(qc::r), - MQT_NAMED_BUILDER(qco::r)}, - QCToQCOTestCase{"SingleControlledR", - MQT_NAMED_BUILDER(qc::singleControlledR), - MQT_NAMED_BUILDER(qco::singleControlledR)}, - QCToQCOTestCase{ - "MultipleControlledR", - MQT_NAMED_BUILDER(qc::multipleControlledR), - MQT_NAMED_BUILDER(qco::multipleControlledR)})); - -// QCToQCO/Operations/StandardGates/RxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRXOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"RX", MQT_NAMED_BUILDER(qc::rx), - MQT_NAMED_BUILDER(qco::rx)}, - QCToQCOTestCase{"SingleControlledRX", - MQT_NAMED_BUILDER(qc::singleControlledRx), - MQT_NAMED_BUILDER(qco::singleControlledRx)}, - QCToQCOTestCase{ - "MultipleControlledRX", - MQT_NAMED_BUILDER(qc::multipleControlledRx), - MQT_NAMED_BUILDER(qco::multipleControlledRx)})); - -// QCToQCO/Operations/StandardGates/RxxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRXXOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"RXX", MQT_NAMED_BUILDER(qc::rxx), - MQT_NAMED_BUILDER(qco::rxx)}, - QCToQCOTestCase{"SingleControlledRXX", - MQT_NAMED_BUILDER(qc::singleControlledRxx), - MQT_NAMED_BUILDER(qco::singleControlledRxx)}, - QCToQCOTestCase{"MultipleControlledRXX", - MQT_NAMED_BUILDER(qc::multipleControlledRxx), - MQT_NAMED_BUILDER(qco::multipleControlledRxx)})); - -// QCToQCO/Operations/StandardGates/RyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRYOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"RY", MQT_NAMED_BUILDER(qc::ry), - MQT_NAMED_BUILDER(qco::ry)}, - QCToQCOTestCase{"SingleControlledRY", - MQT_NAMED_BUILDER(qc::singleControlledRy), - MQT_NAMED_BUILDER(qco::singleControlledRy)}, - QCToQCOTestCase{ - "MultipleControlledRY", - MQT_NAMED_BUILDER(qc::multipleControlledRy), - MQT_NAMED_BUILDER(qco::multipleControlledRy)})); - -// QCToQCO/Operations/StandardGates/RyyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRYYOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"RYY", MQT_NAMED_BUILDER(qc::ryy), - MQT_NAMED_BUILDER(qco::ryy)}, - QCToQCOTestCase{"SingleControlledRYY", - MQT_NAMED_BUILDER(qc::singleControlledRyy), - MQT_NAMED_BUILDER(qco::singleControlledRyy)}, - QCToQCOTestCase{"MultipleControlledRYY", - MQT_NAMED_BUILDER(qc::multipleControlledRyy), - MQT_NAMED_BUILDER(qco::multipleControlledRyy)})); - -// QCToQCO/Operations/StandardGates/RzOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRZOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"RZ", MQT_NAMED_BUILDER(qc::rz), - MQT_NAMED_BUILDER(qco::rz)}, - QCToQCOTestCase{"SingleControlledRZ", - MQT_NAMED_BUILDER(qc::singleControlledRz), - MQT_NAMED_BUILDER(qco::singleControlledRz)}, - QCToQCOTestCase{ - "MultipleControlledRZ", - MQT_NAMED_BUILDER(qc::multipleControlledRz), - MQT_NAMED_BUILDER(qco::multipleControlledRz)})); - -// QCToQCO/Operations/StandardGates/RzxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRZXOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"RZX", MQT_NAMED_BUILDER(qc::rzx), - MQT_NAMED_BUILDER(qco::rzx)}, - QCToQCOTestCase{"SingleControlledRZX", - MQT_NAMED_BUILDER(qc::singleControlledRzx), - MQT_NAMED_BUILDER(qco::singleControlledRzx)}, - QCToQCOTestCase{"MultipleControlledRZX", - MQT_NAMED_BUILDER(qc::multipleControlledRzx), - MQT_NAMED_BUILDER(qco::multipleControlledRzx)})); - -// QCToQCO/Operations/StandardGates/RzzOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCRZZOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"RZZ", MQT_NAMED_BUILDER(qc::rzz), - MQT_NAMED_BUILDER(qco::rzz)}, - QCToQCOTestCase{"SingleControlledRZZ", - MQT_NAMED_BUILDER(qc::singleControlledRzz), - MQT_NAMED_BUILDER(qco::singleControlledRzz)}, - QCToQCOTestCase{"MultipleControlledRZZ", - MQT_NAMED_BUILDER(qc::multipleControlledRzz), - MQT_NAMED_BUILDER(qco::multipleControlledRzz)})); - -// QCToQCO/Operations/StandardGates/SOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCSOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"S", MQT_NAMED_BUILDER(qc::s), - MQT_NAMED_BUILDER(qco::s)}, - QCToQCOTestCase{"SingleControlledS", - MQT_NAMED_BUILDER(qc::singleControlledS), - MQT_NAMED_BUILDER(qco::singleControlledS)}, - QCToQCOTestCase{ - "MultipleControlledS", - MQT_NAMED_BUILDER(qc::multipleControlledS), - MQT_NAMED_BUILDER(qco::multipleControlledS)})); - -// QCToQCO/Operations/StandardGates/SdgOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCSdgOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"Sdg", MQT_NAMED_BUILDER(qc::sdg), - MQT_NAMED_BUILDER(qco::sdg)}, - QCToQCOTestCase{"SingleControlledSdg", - MQT_NAMED_BUILDER(qc::singleControlledSdg), - MQT_NAMED_BUILDER(qco::singleControlledSdg)}, - QCToQCOTestCase{"MultipleControlledSdg", - MQT_NAMED_BUILDER(qc::multipleControlledSdg), - MQT_NAMED_BUILDER(qco::multipleControlledSdg)})); - -// QCToQCO/Operations/StandardGates/SwapOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCSWAPOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"SWAP", MQT_NAMED_BUILDER(qc::swap), - MQT_NAMED_BUILDER(qco::swap)}, - QCToQCOTestCase{"SingleControlledSWAP", - MQT_NAMED_BUILDER(qc::singleControlledSwap), - MQT_NAMED_BUILDER(qco::singleControlledSwap)}, - QCToQCOTestCase{"MultipleControlledSWAP", - MQT_NAMED_BUILDER(qc::multipleControlledSwap), - MQT_NAMED_BUILDER(qco::multipleControlledSwap)})); - -// QCToQCO/Operations/StandardGates/SxOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCSXOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"SX", MQT_NAMED_BUILDER(qc::sx), - MQT_NAMED_BUILDER(qco::sx)}, - QCToQCOTestCase{"SingleControlledSX", - MQT_NAMED_BUILDER(qc::singleControlledSx), - MQT_NAMED_BUILDER(qco::singleControlledSx)}, - QCToQCOTestCase{ - "MultipleControlledSX", - MQT_NAMED_BUILDER(qc::multipleControlledSx), - MQT_NAMED_BUILDER(qco::multipleControlledSx)})); - -// QCToQCO/Operations/StandardGates/SxdgOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCSXdgOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"SXdg", MQT_NAMED_BUILDER(qc::sxdg), - MQT_NAMED_BUILDER(qco::sxdg)}, - QCToQCOTestCase{"SingleControlledSXdg", - MQT_NAMED_BUILDER(qc::singleControlledSxdg), - MQT_NAMED_BUILDER(qco::singleControlledSxdg)}, - QCToQCOTestCase{"MultipleControlledSXdg", - MQT_NAMED_BUILDER(qc::multipleControlledSxdg), - MQT_NAMED_BUILDER(qco::multipleControlledSxdg)})); - -// QCToQCO/Operations/StandardGates/TOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCTOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"T", MQT_NAMED_BUILDER(qc::t_), - MQT_NAMED_BUILDER(qco::t_)}, - QCToQCOTestCase{"SingleControlledT", - MQT_NAMED_BUILDER(qc::singleControlledT), - MQT_NAMED_BUILDER(qco::singleControlledT)}, - QCToQCOTestCase{ - "MultipleControlledT", - MQT_NAMED_BUILDER(qc::multipleControlledT), - MQT_NAMED_BUILDER(qco::multipleControlledT)})); - -// QCToQCO/Operations/StandardGates/TdgOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCTdgOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"Tdg", MQT_NAMED_BUILDER(qc::tdg), - MQT_NAMED_BUILDER(qco::tdg)}, - QCToQCOTestCase{"SingleControlledTdg", - MQT_NAMED_BUILDER(qc::singleControlledTdg), - MQT_NAMED_BUILDER(qco::singleControlledTdg)}, - QCToQCOTestCase{"MultipleControlledTdg", - MQT_NAMED_BUILDER(qc::multipleControlledTdg), - MQT_NAMED_BUILDER(qco::multipleControlledTdg)})); - -// QCToQCO/Operations/StandardGates/U2Op.cpp -INSTANTIATE_TEST_SUITE_P( - QCU2OpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"U2", MQT_NAMED_BUILDER(qc::u2), - MQT_NAMED_BUILDER(qco::u2)}, - QCToQCOTestCase{"SingleControlledU2", - MQT_NAMED_BUILDER(qc::singleControlledU2), - MQT_NAMED_BUILDER(qco::singleControlledU2)}, - QCToQCOTestCase{ - "MultipleControlledU2", - MQT_NAMED_BUILDER(qc::multipleControlledU2), - MQT_NAMED_BUILDER(qco::multipleControlledU2)})); - -// QCToQCO/Operations/StandardGates/UOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCUOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"U", MQT_NAMED_BUILDER(qc::u), - MQT_NAMED_BUILDER(qco::u)}, - QCToQCOTestCase{"SingleControlledU", - MQT_NAMED_BUILDER(qc::singleControlledU), - MQT_NAMED_BUILDER(qco::singleControlledU)}, - QCToQCOTestCase{ - "MultipleControlledU", - MQT_NAMED_BUILDER(qc::multipleControlledU), - MQT_NAMED_BUILDER(qco::multipleControlledU)})); - -// QCToQCO/Operations/StandardGates/XOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCXOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"X", MQT_NAMED_BUILDER(qc::x), - MQT_NAMED_BUILDER(qco::x)}, - QCToQCOTestCase{"SingleControlledX", - MQT_NAMED_BUILDER(qc::singleControlledX), - MQT_NAMED_BUILDER(qco::singleControlledX)}, - QCToQCOTestCase{"MultipleControlledX", - MQT_NAMED_BUILDER(qc::multipleControlledX), - MQT_NAMED_BUILDER(qco::multipleControlledX)}, - QCToQCOTestCase{"RepeatedControlledX", - MQT_NAMED_BUILDER(qc::repeatedControlledX), - MQT_NAMED_BUILDER(qco::repeatedControlledX)})); - -// QCToQCO/Operations/StandardGates/XxMinusYyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCXXMinusYYOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"XXMinusYY", MQT_NAMED_BUILDER(qc::xxMinusYY), - MQT_NAMED_BUILDER(qco::xxMinusYY)}, - QCToQCOTestCase{"SingleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY), - MQT_NAMED_BUILDER(qco::singleControlledXxMinusYY)}, - QCToQCOTestCase{"MultipleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::multipleControlledXxMinusYY), - MQT_NAMED_BUILDER(qco::multipleControlledXxMinusYY)})); - -// QCToQCO/Operations/StandardGates/XxPlusYyOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCXXPlusYYOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"XXPlusYY", MQT_NAMED_BUILDER(qc::xxPlusYY), - MQT_NAMED_BUILDER(qco::xxPlusYY)}, - QCToQCOTestCase{"SingleControlledXXPlusYY", - MQT_NAMED_BUILDER(qc::singleControlledXxPlusYY), - MQT_NAMED_BUILDER(qco::singleControlledXxPlusYY)}, - QCToQCOTestCase{"MultipleControlledXXPlusYY", - MQT_NAMED_BUILDER(qc::multipleControlledXxPlusYY), - MQT_NAMED_BUILDER(qco::multipleControlledXxPlusYY)})); - -// QCToQCO/Operations/StandardGates/YOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCYOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"Y", MQT_NAMED_BUILDER(qc::y), - MQT_NAMED_BUILDER(qco::y)}, - QCToQCOTestCase{"SingleControlledY", - MQT_NAMED_BUILDER(qc::singleControlledY), - MQT_NAMED_BUILDER(qco::singleControlledY)}, - QCToQCOTestCase{ - "MultipleControlledY", - MQT_NAMED_BUILDER(qc::multipleControlledY), - MQT_NAMED_BUILDER(qco::multipleControlledY)})); - -// QCToQCO/Operations/StandardGates/ZOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCZOpTest, QCToQCOTest, - testing::Values(QCToQCOTestCase{"Z", MQT_NAMED_BUILDER(qc::z), - MQT_NAMED_BUILDER(qco::z)}, - QCToQCOTestCase{"SingleControlledZ", - MQT_NAMED_BUILDER(qc::singleControlledZ), - MQT_NAMED_BUILDER(qco::singleControlledZ)}, - QCToQCOTestCase{ - "MultipleControlledZ", - MQT_NAMED_BUILDER(qc::multipleControlledZ), - MQT_NAMED_BUILDER(qco::multipleControlledZ)})); - -// QCToQCO/Operations/MeasureOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCMeasureOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"SingleMeasurementToSingleBit", - MQT_NAMED_BUILDER(qc::singleMeasurementToSingleBit), - MQT_NAMED_BUILDER(qco::singleMeasurementToSingleBit)}, - QCToQCOTestCase{"RepeatedMeasurementToSameBit", - MQT_NAMED_BUILDER(qc::repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(qco::repeatedMeasurementToSameBit)}, - QCToQCOTestCase{ - "RepeatedMeasurementToDifferentBits", - MQT_NAMED_BUILDER(qc::repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(qco::repeatedMeasurementToDifferentBits)}, - QCToQCOTestCase{ - "MultipleClassicalRegistersAndMeasurements", - MQT_NAMED_BUILDER(qc::multipleClassicalRegistersAndMeasurements), - MQT_NAMED_BUILDER(qco::multipleClassicalRegistersAndMeasurements)}, - QCToQCOTestCase{"PartialMeasurementToRegister", - MQT_NAMED_BUILDER(qc::partialMeasurementToRegister), - MQT_NAMED_BUILDER(qco::partialMeasurementToRegister)}, - QCToQCOTestCase{"DynamicallyIndexedMeasurement", - MQT_NAMED_BUILDER(qc::dynamicallyIndexedMeasurement), - MQT_NAMED_BUILDER(qco::dynamicallyIndexedMeasurement), - true}, - QCToQCOTestCase{"MeasurementWithoutRegisters", - MQT_NAMED_BUILDER(qc::measurementWithoutRegisters), - MQT_NAMED_BUILDER(qco::measurementWithoutRegisters)})); - -// QCToQCO/Operations/ResetOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCResetOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"ResetQubitAfterSingleOp", - MQT_NAMED_BUILDER(qc::resetQubitAfterSingleOp), - MQT_NAMED_BUILDER(qco::resetQubitAfterSingleOp)}, - QCToQCOTestCase{ - "ResetMultipleQubitsAfterSingleOp", - MQT_NAMED_BUILDER(qc::resetMultipleQubitsAfterSingleOp), - MQT_NAMED_BUILDER(qco::resetMultipleQubitsAfterSingleOp)}, + MQT_NAMED_BUILDER(qco::emptyQCO)}, + QCToQCOTestCase{"Identity", MQT_NAMED_BUILDER(qc::identity), + MQT_NAMED_BUILDER(qco::alloc1QubitRegister)}, QCToQCOTestCase{"RepeatedResetAfterSingleOp", MQT_NAMED_BUILDER(qc::repeatedResetAfterSingleOp), - MQT_NAMED_BUILDER(qco::resetQubitAfterSingleOp)})); - -// QCToQCO/Operations/IfOp.cpp -INSTANTIATE_TEST_SUITE_P( - SCFIfOpTest, QCToQCOTest, - testing::Values( + MQT_NAMED_BUILDER(qco::resetQubitAfterSingleOp)}, QCToQCOTestCase{"SimpleIfOp", MQT_NAMED_BUILDER(qc::simpleIf), MQT_NAMED_BUILDER(qco::simpleIfCompleteTensorState), true}, @@ -2162,41 +1720,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQCOTestCase{"IfWithCreg", MQT_NAMED_BUILDER(qc::ifWithCreg), MQT_NAMED_BUILDER(qco::ifWithCregCompleteTensorState), true}, - QCToQCOTestCase{"NestedIfOpForLoop", - MQT_NAMED_BUILDER(qc::nestedIfOpForLoop), - MQT_NAMED_BUILDER(qco::nestedIfOpForLoop), true})); - -// QCToQCO/Operations/IndexSwitchOp.cpp -INSTANTIATE_TEST_SUITE_P( - QCOIndexSwitchOpTest, QCToQCOTest, - testing::Values( QCToQCOTestCase{ "SimpleIndexSwitchOp", MQT_NAMED_BUILDER(qc::simpleIndexSwitch), MQT_NAMED_BUILDER(qco::simpleIndexSwitchCompleteTensorState), true}, QCToQCOTestCase{ "IndexSwitchMultiCase", MQT_NAMED_BUILDER(qc::indexSwitchMultiCase), MQT_NAMED_BUILDER(qco::indexSwitchMultiCaseCompleteTensorState), - true})); - -// QCToQCO/Operations/WhileOp.cpp -INSTANTIATE_TEST_SUITE_P( - SCFWhileOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"SimpleWhile", MQT_NAMED_BUILDER(qc::simpleWhileReset), - MQT_NAMED_BUILDER(qco::simpleWhileReset)}, - QCToQCOTestCase{"SimpleDoWhile", - MQT_NAMED_BUILDER(qc::simpleDoWhileReset), - MQT_NAMED_BUILDER(qco::simpleDoWhileReset)})); - -// QCToQCO/Operations/ForOp.cpp -INSTANTIATE_TEST_SUITE_P( - SCFForOpTest, QCToQCOTest, - testing::Values( - QCToQCOTestCase{"SimpleForLoop", MQT_NAMED_BUILDER(qc::simpleForLoop), - MQT_NAMED_BUILDER(qco::simpleForLoop), true}, - QCToQCOTestCase{"NestedForLoopIfOp", - MQT_NAMED_BUILDER(qc::nestedForLoopIfOp), - MQT_NAMED_BUILDER(qco::nestedForLoopIfOp), true}, + true}, QCToQCOTestCase{ "NestedForLoopWhileOp", MQT_NAMED_BUILDER(qc::nestedForLoopWhileOp), MQT_NAMED_BUILDER(qco::nestedForLoopWhileOpCompleteTensorState), @@ -2206,10 +1736,6 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(qc::nestedForLoopSwitchOp), MQT_NAMED_BUILDER(qco::nestedForLoopSwitchOpCompleteTensorState), true}, - QCToQCOTestCase{ - "NestedForLoopCtrlOpWithSeparateQubit", - MQT_NAMED_BUILDER(qc::nestedForLoopCtrlOpWithSeparateQubit), - MQT_NAMED_BUILDER(qco::nestedForLoopCtrlOpWithSeparateQubit), true}, QCToQCOTestCase{ "NestedForLoopCtrlOpWithExtractedQubit", MQT_NAMED_BUILDER(qc::nestedForLoopCtrlOpWithExtractedQubit), diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 5b0df25b3c..8d3deb6bb2 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -367,7 +367,7 @@ TEST_F(QCTest, BuilderSupportsIndependentClassicalRegisterInitialization) { "undefined"); } -TEST_F(QCTest, BuilderDeallocatesDynamicResourcesDeterministically) { +TEST_F(QCTest, BuilderDisposesOnlyUnreturnedResourcesInAllocationOrder) { QCProgramBuilder builder(context.get()); builder.initialize(); @@ -378,8 +378,17 @@ TEST_F(QCTest, BuilderDeallocatesDynamicResourcesDeterministically) { allocatedRegisters.push_back(builder.allocQubitRegisterStorage(1)); } - auto moduleOp = builder.finalize(); + SmallVector returned{ + allocatedQubits.pop_back_val(), + allocatedRegisters.pop_back_val(), + }; + builder.retype(ValueRange(returned).getTypes()); + auto moduleOp = builder.finalize(returned); ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto mainFunc = mlir::mqt::getEntryPoint(*moduleOp); + auto terminator = cast(mainFunc.getBody().front().back()); + EXPECT_TRUE(llvm::equal(terminator.getOperands(), returned)); SmallVector deallocatedQubits; SmallVector deallocatedRegisters; diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index a3179f7a61..c16339ee47 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -693,6 +693,60 @@ TEST_F(QCOTest, BuilderRejectsIncompleteRegisterBoundaries) { } } +TEST_F(QCOTest, BuilderRejectsQuantumCapturesAndEscapes) { + for (unsigned scenario = 0; scenario < 4; ++scenario) { + SCOPED_TRACE(scenario); + EXPECT_DEATH( + ([&] { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto qubit = builder.allocQubit(); + auto tensor = builder.qtensorAlloc(1); + if (scenario < 2) { + builder.qcoIf(true, ValueRange{}, [&](ValueRange) { + if (scenario == 0) { + builder.h(qubit); + } else { + builder.qtensorExtract(tensor, 0); + } + return SmallVector{}; + }); + } else if (scenario == 2) { + builder.qcoIf(true, tensor, [&](Value argument) { + return builder.qtensorExtract(argument, 0).first; + }); + builder.finalize(); + } else { + builder.inv(qubit, [&](Value argument) { + builder.qtensorExtract(tensor, 0); + return argument; + }); + } + }()), + "Quantum values must be passed explicitly through region arguments"); + } +} + +TEST_F(QCOTest, BuilderRejectsChangedTensorSlots) { + for (unsigned scenario = 0; scenario < 3; ++scenario) { + SCOPED_TRACE(scenario); + EXPECT_DEATH(([&] { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto [tensor, qubit] = + builder.qtensorExtract(builder.qtensorAlloc(2), 0); + if (scenario == 1) { + tensor = builder.qtensorAlloc(2); + } else if (scenario == 2) { + qubit = builder.allocQubit(); + } + builder.qtensorInsert(qubit, tensor, scenario == 0 ? 1 : 0); + }()), + "Inserted qubit must (return to its original slot|belong to " + "the destination register)"); + } +} + TEST_F(QCOTest, BuilderRejectsChangedTensorRegisterAssociations) { EXPECT_DEATH(([&] { QCOProgramBuilder builder(context.get()); diff --git a/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp b/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp index 1f9830cece..e0d7041f4f 100644 --- a/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp +++ b/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp @@ -336,6 +336,7 @@ TEST_F(QTensorTest, NestedRegisterAccessInsideModifierFailsVerification) { namespace { enum class AdjacentIndexKind : std::uint8_t { EqualConstants, + DifferentConstants, IdenticalDynamicValue, PotentiallyAliasingDynamicValues, }; @@ -358,9 +359,11 @@ buildAdjacentInsertExtractProgram(MLIRContext* context, Value insertIndex; Value extractIndex; - if (indexKind == AdjacentIndexKind::EqualConstants) { + if (indexKind == AdjacentIndexKind::EqualConstants || + indexKind == AdjacentIndexKind::DifferentConstants) { insertIndex = arith::ConstantIndexOp::create(b, 0); - extractIndex = arith::ConstantIndexOp::create(b, 0); + extractIndex = arith::ConstantIndexOp::create( + b, indexKind == AdjacentIndexKind::EqualConstants ? 0 : 1); } else { insertIndex = block->getArgument(0); extractIndex = indexKind == AdjacentIndexKind::IdenticalDynamicValue @@ -394,11 +397,7 @@ static LogicalResult canonicalize(ModuleOp moduleOp) { static OwningOpRef buildTwoQubitInsertChainProgram(MLIRContext* context, - const bool reverseInsertOrder, - const bool swapInsertTargets) { - const int64_t q0Target = swapInsertTargets ? 1 : 0; - const int64_t q1Target = swapInsertTargets ? 0 : 1; - + const bool reverseInsertOrder) { QCOProgramBuilder builder(context); builder.initialize(); @@ -410,11 +409,11 @@ buildTwoQubitInsertChainProgram(MLIRContext* context, std::tie(tensor, q1) = builder.qtensorExtract(tensor, 1); if (reverseInsertOrder) { - tensor = builder.qtensorInsert(q1, tensor, q1Target); - tensor = builder.qtensorInsert(q0, tensor, q0Target); + tensor = builder.qtensorInsert(q1, tensor, 1); + tensor = builder.qtensorInsert(q0, tensor, 0); } else { - tensor = builder.qtensorInsert(q0, tensor, q0Target); - tensor = builder.qtensorInsert(q1, tensor, q1Target); + tensor = builder.qtensorInsert(q0, tensor, 0); + tensor = builder.qtensorInsert(q1, tensor, 1); } return builder.finalize(); @@ -468,41 +467,31 @@ buildResetWithSameIndexInsertProgram(MLIRContext* context, namespace { -TEST_F(QTensorTest, AdjacentInsertExtractFoldsEqualConstants) { - auto moduleOp = buildAdjacentInsertExtractProgram( - context.get(), AdjacentIndexKind::EqualConstants); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - ASSERT_TRUE(succeeded(canonicalize(*moduleOp))); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - EXPECT_EQ(countOps(*moduleOp), 1U); - EXPECT_EQ(countOps(*moduleOp), 1U); -} - -TEST_F(QTensorTest, AdjacentInsertExtractFoldsIdenticalDynamicValue) { - auto moduleOp = buildAdjacentInsertExtractProgram( - context.get(), AdjacentIndexKind::IdenticalDynamicValue); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - ASSERT_TRUE(succeeded(canonicalize(*moduleOp))); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - EXPECT_EQ(countOps(*moduleOp), 1U); - EXPECT_EQ(countOps(*moduleOp), 1U); -} - -TEST_F(QTensorTest, AdjacentInsertExtractKeepsPotentialDynamicAlias) { - auto moduleOp = buildAdjacentInsertExtractProgram( - context.get(), AdjacentIndexKind::PotentiallyAliasingDynamicValues); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - ASSERT_TRUE(succeeded(canonicalize(*moduleOp))); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - EXPECT_EQ(countOps(*moduleOp), 2U); - EXPECT_EQ(countOps(*moduleOp), 2U); +TEST_F(QTensorTest, AdjacentInsertExtractFoldsOnlyEqualIndices) { + for (const auto& [name, indexKind, expectedCount] : { + std::tuple{"equal constants", AdjacentIndexKind::EqualConstants, 1U}, + std::tuple{"different constants", + AdjacentIndexKind::DifferentConstants, 2U}, + std::tuple{"same dynamic value", + AdjacentIndexKind::IdenticalDynamicValue, 1U}, + std::tuple{"potential dynamic alias", + AdjacentIndexKind::PotentiallyAliasingDynamicValues, 2U}, + }) { + SCOPED_TRACE(name); + auto moduleOp = buildAdjacentInsertExtractProgram(context.get(), indexKind); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + ASSERT_TRUE(succeeded(canonicalize(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + EXPECT_EQ(countOps(*moduleOp), expectedCount); + EXPECT_EQ(countOps(*moduleOp), expectedCount); + } } TEST_F(QTensorTest, InsertChainCanonicalizationRemainsLocal) { - auto program = buildTwoQubitInsertChainProgram(context.get(), false, false); + auto program = buildTwoQubitInsertChainProgram(context.get(), false); ASSERT_TRUE(program); ASSERT_TRUE(verify(*program).succeeded()); ASSERT_TRUE(succeeded(qco::verifyLinearity(*program))); @@ -510,7 +499,7 @@ TEST_F(QTensorTest, InsertChainCanonicalizationRemainsLocal) { ASSERT_TRUE(verify(*program).succeeded()); ASSERT_TRUE(succeeded(qco::verifyLinearity(*program))); - auto reference = buildTwoQubitInsertChainProgram(context.get(), true, false); + auto reference = buildTwoQubitInsertChainProgram(context.get(), true); ASSERT_TRUE(reference); ASSERT_TRUE(verify(*reference).succeeded()); ASSERT_TRUE(succeeded(qco::verifyLinearity(*reference))); @@ -522,23 +511,6 @@ TEST_F(QTensorTest, InsertChainCanonicalizationRemainsLocal) { EXPECT_EQ(countOps(*reference), 0U); } -TEST_F(QTensorTest, InsertChainDifferentAssignmentsNotEquivalent) { - auto program = buildTwoQubitInsertChainProgram(context.get(), false, false); - ASSERT_TRUE(program); - EXPECT_TRUE(verify(*program).succeeded()); - EXPECT_TRUE(runQCOCleanupPipeline(program.get()).succeeded()); - EXPECT_TRUE(verify(*program).succeeded()); - - auto reference = buildTwoQubitInsertChainProgram(context.get(), true, true); - ASSERT_TRUE(reference); - EXPECT_TRUE(verify(*reference).succeeded()); - EXPECT_TRUE(runQCOCleanupPipeline(reference.get()).succeeded()); - EXPECT_TRUE(verify(*reference).succeeded()); - - EXPECT_FALSE( - areModulesEquivalentWithPermutations(program.get(), reference.get())); -} - TEST_F(QTensorTest, ResetAfterExtractThroughCommutingInsertIsEliminated) { auto program = buildResetWithCommutingInsertProgram(context.get(), true); ASSERT_TRUE(program); @@ -851,18 +823,10 @@ INSTANTIATE_TEST_SUITE_P( "QTensorExtractInsertSameIndex", MQT_NAMED_BUILDER(qtensorExtractInsertSameIndex), MQT_NAMED_BUILDER(qtensorAlloc)}, - QTensorIntegrationTestCase{ - "QTensorExtractInsertIndexMismatch", - MQT_NAMED_BUILDER(qtensorExtractInsertIndexMismatch), - MQT_NAMED_BUILDER(qtensorExtractInsertIndexMismatch)}, QTensorIntegrationTestCase{ "QTensorInsertExtractSameIndex", MQT_NAMED_BUILDER(qtensorInsertExtractSameIndex), MQT_NAMED_BUILDER(qtensorInsert)}, - QTensorIntegrationTestCase{ - "QTensorInsertExtractIndexMismatch", - MQT_NAMED_BUILDER(qtensorInsertExtractIndexMismatch), - MQT_NAMED_BUILDER(qtensorInsertExtractIndexMismatch)}, QTensorIntegrationTestCase{"QTensorAlternativeInsertChain", MQT_NAMED_BUILDER(qtensorAlternativeChain), MQT_NAMED_BUILDER(qtensorChain)})); diff --git a/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp b/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp index bd506917bb..0bebb65b4b 100644 --- a/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp +++ b/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp @@ -11,6 +11,8 @@ #include "mqt/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mqt/Dialect/QCO/IR/QCODialect.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" +#include "mqt/Dialect/QCO/QCOUtils.h" +#include "mqt/Dialect/QCO/Utils/WireIterator.h" #include "mqt/Dialect/QTensor/IR/QTensorDialect.h" #include "mqt/Dialect/QTensor/IR/QTensorOps.h" #include "mqt/Dialect/QTensor/Utils/TensorIterator.h" @@ -378,79 +380,63 @@ TEST_F(TensorIteratorTest, TraversesMixedResultConditionals) { } TEST_F(TensorIteratorTest, TraversesWhileCarriedTensors) { - QCOProgramBuilder builder(context.get()); - builder.initialize(); - - auto scalar0 = builder.floatConstant(1.0); - auto tensor0 = builder.qtensorAlloc(2); - auto tensor1 = builder.qtensorAlloc(3); - auto loop = scf::WhileOp::create( - builder, builder.getLoc(), - TypeRange{builder.getI64Type(), tensor1.getType(), tensor0.getType()}, - ValueRange{scalar0, tensor0, tensor1}); - const SmallVector locations(3, builder.getLoc()); - auto* before = builder.createBlock( - &loop.getBefore(), {}, ValueRange{scalar0, tensor0, tensor1}.getTypes(), - locations); - builder.setInsertionPointToStart(before); - auto scalar1 = builder.intConstant(1); - scf::ConditionOp::create( - builder, builder.getLoc(), builder.boolConstant(false), - ValueRange{scalar1, before->getArgument(2), before->getArgument(1)}); - auto* after = builder.createBlock(&loop.getAfter(), {}, loop.getResultTypes(), - locations); - builder.setInsertionPointToStart(after); - scf::YieldOp::create(builder, builder.getLoc(), - ValueRange{ - builder.floatConstant(2.0), - after->getArgument(2), - after->getArgument(1), - }); - builder.setInsertionPointAfter(loop); - auto tensor0Result = loop.getResult(2); - auto tensor1Result = loop.getResult(1); - qtensor::DeallocOp::create(builder, builder.getLoc(), tensor0Result); - qtensor::DeallocOp::create(builder, builder.getLoc(), tensor1Result); - ASSERT_TRUE(succeeded(verify(loop))); - - TensorIterator beforeRegionIterator( - cast>(before->getArgument(1))); - ++beforeRegionIterator; - ASSERT_TRUE(isa(beforeRegionIterator.operation())); - ASSERT_EQ(beforeRegionIterator.tensor(), nullptr); - - TensorIterator iterator(cast>(tensor0)); - ASSERT_EQ(iterator.operation(), tensor0.getDefiningOp()); - ASSERT_EQ(iterator.tensor(), tensor0); - - ++iterator; - ASSERT_TRUE(isa(iterator.operation())); - ASSERT_EQ(iterator.tensor(), tensor0Result); - - ++iterator; - ASSERT_TRUE(isa(iterator.operation())); - ASSERT_EQ(iterator.tensor(), nullptr); - - ++iterator; - ASSERT_EQ(iterator, std::default_sentinel); - - --iterator; - ASSERT_TRUE(isa(iterator.operation())); - ASSERT_EQ(iterator.tensor(), nullptr); - - --iterator; - ASSERT_TRUE(isa(iterator.operation())); - ASSERT_EQ(iterator.tensor(), tensor0Result); - - --iterator; - ASSERT_EQ(iterator.operation(), tensor0.getDefiningOp()); - ASSERT_EQ(iterator.tensor(), tensor0); - - TensorIterator swapped(cast>(tensor1)); - ++swapped; - ASSERT_TRUE(isa(swapped.operation())); - ASSERT_EQ(swapped.tensor(), tensor1Result); - --swapped; - ASSERT_EQ(swapped.operation(), tensor1.getDefiningOp()); - ASSERT_EQ(swapped.tensor(), tensor1); + auto moduleOp = parseSourceString(R"mlir( + func.func private @touch(%t: tensor<2x!qco.qubit>) -> tensor<2x!qco.qubit> { + return %t : tensor<2x!qco.qubit> + } + func.func @main() { + %c2 = arith.constant 2 : index + %c3 = arith.constant 3 : index + %zero = arith.constant 0.0 : f64 + %false = arith.constant false + %t0 = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %q = qco.alloc : !qco.qubit + %t1 = qtensor.alloc(%c3) : tensor<3x!qco.qubit> + %out:4 = scf.while (%c = %zero, %a = %t0, %b = %q, %d = %t1) + : (f64, tensor<2x!qco.qubit>, !qco.qubit, tensor<3x!qco.qubit>) + -> (tensor<2x!qco.qubit>, !qco.qubit, i1, tensor<3x!qco.qubit>) { + %called = func.call @touch(%a) : (tensor<2x!qco.qubit>) -> tensor<2x!qco.qubit> + scf.condition(%false) %called, %b, %false, %d + : tensor<2x!qco.qubit>, !qco.qubit, i1, tensor<3x!qco.qubit> + } do { + ^bb0(%a: tensor<2x!qco.qubit>, %b: !qco.qubit, %c: i1, %d: tensor<3x!qco.qubit>): + scf.yield %zero, %a, %b, %d + : f64, tensor<2x!qco.qubit>, !qco.qubit, tensor<3x!qco.qubit> + } + qtensor.dealloc %out#0 : tensor<2x!qco.qubit> + qco.sink %out#1 : !qco.qubit + qtensor.dealloc %out#3 : tensor<3x!qco.qubit> + return + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); + scf::WhileOp loop; + moduleOp->walk([&](scf::WhileOp op) { loop = op; }); + ASSERT_TRUE(loop); + qco::WireIterator wire(loop.getInits()[2]); + ++wire; + EXPECT_EQ(wire.qubit(), loop.getResult(1)); + --wire; + EXPECT_EQ(wire.qubit(), loop.getInits()[2]); + for (auto [input, output] : {std::pair{1U, 0U}, std::pair{3U, 3U}}) { + auto tensor = cast>(loop.getInits()[input]); + TensorIterator iterator(tensor); + ++iterator; + EXPECT_EQ(iterator.operation(), loop.getOperation()); + EXPECT_EQ(iterator.tensor(), loop.getResult(output)); + ++iterator; + EXPECT_TRUE(isa(iterator.operation())); + EXPECT_EQ(iterator.tensor(), nullptr); + ++iterator; + EXPECT_EQ(iterator, std::default_sentinel); + --iterator; + --iterator; + EXPECT_EQ(iterator.tensor(), loop.getResult(output)); + --iterator; + EXPECT_EQ(iterator.tensor(), tensor); + EXPECT_EQ(iterator.operation(), tensor.getDefiningOp()); + } } diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index 0ee7133358..37f9c62049 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -4530,13 +4530,6 @@ Value qtensorInsert(QCOProgramBuilder& b) { return measureAndReturn(b, {}); } -Value qtensorExtractInsertIndexMismatch(QCOProgramBuilder& b) { - auto qtensor = b.qtensorAlloc(3); - auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); - (void)b.qtensorInsert(q0, extractOutTensor, 1); - return measureAndReturn(b, {}); -} - Value qtensorExtractInsertSameIndex(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); @@ -4544,16 +4537,6 @@ Value qtensorExtractInsertSameIndex(QCOProgramBuilder& b) { return measureAndReturn(b, {}); } -Value qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b) { - auto qtensor = b.qtensorAlloc(3); - auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); - auto q1 = b.h(q0); - auto insertOutTensor = b.qtensorInsert(q1, extractOutTensor, 0); - auto [extractOutTensor1, q2] = b.qtensorExtract(insertOutTensor, 1); - (void)b.qtensorInsert(q2, extractOutTensor1, 0); - return measureAndReturn(b, {}); -} - Value qtensorInsertExtractSameIndex(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index 3550463107..837d60fc0a 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -1544,17 +1544,9 @@ Value qtensorExtract(QCOProgramBuilder& b); /// Inserts a qubit into a tensor. Value qtensorInsert(QCOProgramBuilder& b); -/// Extracts a qubit from a tensor and inserts it immediately at a different -/// index. -Value qtensorExtractInsertIndexMismatch(QCOProgramBuilder& b); - /// Extracts a qubit from a tensor and inserts it immediately at the same index. Value qtensorExtractInsertSameIndex(QCOProgramBuilder& b); -/// Inserts a qubit into a tensor and extracts it immediately at a different -/// index. -Value qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b); - /// Inserts a qubit into a tensor and extracts it immediately at the same index. Value qtensorInsertExtractSameIndex(QCOProgramBuilder& b); From 217874933586cc93170f89e14cd96a6ecd2d2683 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 18 Oct 2026 21:41:06 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=90=9B=20Make=20tensor-slot=20death?= =?UTF-8?q?=20checks=20portable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Select the expected diagnostic for each tensor-slot rejection scenario. GoogleTest's Windows regex engine rejects grouping and alternation, so the shared matcher failed despite the builder emitting the correct error. Retain all three cases and check their specific diagnostics. Validate the matcher with GoogleTest's simple regex engine and the complete QCO IR suite. Assisted-by: GPT-6 via Codex --- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index c16339ee47..8d910e7a06 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -730,20 +730,22 @@ TEST_F(QCOTest, BuilderRejectsQuantumCapturesAndEscapes) { TEST_F(QCOTest, BuilderRejectsChangedTensorSlots) { for (unsigned scenario = 0; scenario < 3; ++scenario) { SCOPED_TRACE(scenario); - EXPECT_DEATH(([&] { - QCOProgramBuilder builder(context.get()); - builder.initialize(); - auto [tensor, qubit] = - builder.qtensorExtract(builder.qtensorAlloc(2), 0); - if (scenario == 1) { - tensor = builder.qtensorAlloc(2); - } else if (scenario == 2) { - qubit = builder.allocQubit(); - } - builder.qtensorInsert(qubit, tensor, scenario == 0 ? 1 : 0); - }()), - "Inserted qubit must (return to its original slot|belong to " - "the destination register)"); + EXPECT_DEATH( + ([&] { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto [tensor, qubit] = + builder.qtensorExtract(builder.qtensorAlloc(2), 0); + if (scenario == 1) { + tensor = builder.qtensorAlloc(2); + } else if (scenario == 2) { + qubit = builder.allocQubit(); + } + builder.qtensorInsert(qubit, tensor, scenario == 0 ? 1 : 0); + }()), + scenario == 0 + ? "Inserted qubit must return to its original slot" + : "Inserted qubit must belong to the destination register"); } } From 1bebf328e74030b6a9573b42bae550afd26b0cf9 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 19 Oct 2026 09:22:16 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9A=A1=20Simplify=20wire=20traversal=20a?= =?UTF-8?q?nd=20QC=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Use operand positions for unitary wire traversal and restore the shared range in read-only consumers. Keep tail-first erasure explicit. Wide traversal now scales linearly while generic calls and unknown carriers remain boundaries. Keep structured capture order in one record, derive modifier yields from live block arguments, and replace unused type-conversion machinery with exact type helpers. Preserve graceful rejection of misplaced yields and cover it with a small regression. Record paired performance measurements and their limits. Assisted-by: GPT-6 via Codex --- .../audits/qc-qco-wire-followup-20261019.md | 127 ++++++++++ .../mqt/Dialect/QCO/IR/QCOInterfaces.td | 3 + .../mqt/Dialect/QCO/Utils/WireIterator.h | 6 +- mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 238 +++++++----------- .../QCO/Transforms/Mapping/Mapping.cpp | 4 +- .../FuseSingleQubitUnitaryRuns.cpp | 15 +- .../MergeSingleQubitRotationGates.cpp | 13 +- mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 38 +-- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 11 + .../Dialect/QCO/Utils/test_wireiterator.cpp | 35 ++- 10 files changed, 284 insertions(+), 206 deletions(-) create mode 100644 .agent/audits/qc-qco-wire-followup-20261019.md diff --git a/.agent/audits/qc-qco-wire-followup-20261019.md b/.agent/audits/qc-qco-wire-followup-20261019.md new file mode 100644 index 0000000000..1e37edc61a --- /dev/null +++ b/.agent/audits/qc-qco-wire-followup-20261019.md @@ -0,0 +1,127 @@ +# QC/QCO wire and converter follow-up + +Date: 2026-10-19. Follow-up to the +[combined audit](qc-qco-combined-2716-20261018.md) for +[PR #2716](https://github.com/munich-quantum-toolkit/core/pull/2716). + +## Changes and contracts + +Neither `WireIterator` nor `WireRange` had been deleted. The earlier audit +replaced three read-only range uses with direct scans, leaving the range unused +in production. Those bundled measurements did not isolate a benefit from the +caller changes. This follow-up restores `WireRange` in rotation-chain +collection, single-qubit fusion scanning, and measurement routing. The direct +tail-first fusion deletion loop remains: it obtains the predecessor before +erasing the current operation. + +The iterator now uses its sole `OpOperand` position to select the corresponding +unitary output, respecting the quantum operand range offset. Walking all W wires +through a W-wide barrier or unitary call therefore requires O(W), rather than +O(W²), correspondence work. Redundant tail classification is removed; the +existing default branch preserves the same terminal input value. Backward lookup +already uses result positions for wide operations and remains unchanged. Scalar +QCO qubits, linear def-use chains, and positional unitary wire identity +(including SWAP) are documented requirements. Generic calls and unknown carriers +remain boundaries. + +The converter removes three bookkeeping maps: structured capture order now lives +in one record before and after conversion, and modifier yields use their +preserved block arguments. The record moves only after region seeding, avoiding +references invalidated by map growth. Scalar capture filtering uses a vector +because the source capture set is already unique; register identities still need +deduplication. Two total type helpers replace `TypeConverter` and its pattern +plumbing without broadening the supported type domain. Region-local live +mappings, preflight validation, source retention, and deferred erasure stay. + +Independent review found that removing modifier bookkeeping also removed an +implicit rejection: a misplaced `qc.yield` can verify, then assert while +resolving classical block arguments. An explicit converted-modifier parent check +preserves graceful rejection. The frozen baseline exits 1, the intermediate +prototype aborts, and the final CLI exits 1 again. One small regression test +covers this; an existing wire-call test now checks two wires and a leading +classical operand. Final specialist reviews found no remaining issue. + +This follow-up removes 73 net production lines, including 58 in `QCToQCO.cpp`. +The complete PR removes 219 production and 409 test lines against its refreshed +upstream base, retaining the shared 132 forward and 133 reverse conversion +cases. + +## Measurements + +The baseline is `217874933586cc93170f89e14cd96a6ecd2d2683`: the prior PR content +rebased onto upstream `3f1ccfd69b12f101587be35c9f5eb05c42adf211`. All variants +use Clang 23.1.4, LLVM/MLIR 23.1.0, release `-O3`, assertions, and ThinLTO on +DGX Spark. Five fresh-process rounds rotate variant order on CPU 0. The host and +core are shared, not reserved; timings are diagnostic. Our builds and test jobs +were stopped during measurement. + +Wire timings exclude parsing and setup. Forward traversal includes allocations, +the unitary operation, and sinks; backward traversal starts at the final +produced qubits. All 90 samples agree on traversal counts. The following medians +are normalized to one traversal of all wires, or the entire single-qubit chain: + +| Case | Baseline forward (ms) | Updated forward (ms) | +| ----------------------------------- | --------------------: | -------------------: | +| Barrier, 32 wires | 0.002107 | 0.001552 | +| Barrier, 512 wires | 0.211623 | 0.024890 | +| Barrier, 4,096 wires | 12.365600 | 0.198579 | +| Barrier, 8,192 wires | 48.888000 | 0.394025 | +| Unitary call, 32 wires | 0.001459 | 0.001676 | +| Unitary call, 512 wires | 0.060364 | 0.027184 | +| Unitary call, 4,096 wires | 4.550105 | 0.219462 | +| Unitary call, 8,192 wires | 19.587000 | 0.437436 | +| Single-qubit H chain, 100,000 gates | 2.722025 | 2.560005 | + +The wide barrier/call improvements reach 124×/45×. This is not a universal +speedup: the 32-wire call is 15% slower (about 0.22 microseconds per traversal). +Backward chain time is effectively unchanged, 2.723 versus 2.769 ms. + +The conversion/pipeline harness compares baseline, simplified converter and +iterator with direct callers, and the same code with restored range callers. It +uses the previous audit's frozen inputs and configuration. Output IR hashes and +gate counts must agree. RSS is the process peak including imported/copied IR, +sampled before serialization; it is not converter-only live memory. + +All 105 conversion/pipeline samples agree on output IR hashes and gate counts. +Medians below compare the baseline with the final range variant. + +| Case | Baseline (ms) | Final (ms) | Baseline RSS (MiB) | Final RSS (MiB) | +| ------------------- | ------------: | ---------: | -----------------: | --------------: | +| flat-1m | 663.080 | 648.090 | 492.07 | 492.00 | +| register-loads-100k | 728.253 | 726.299 | 166.30 | 166.11 | +| sibling-loops-2k-32 | 90.842 | 88.852 | 59.27 | 57.59 | +| wide-helper-8192 | 2.578 | 2.553 | 17.90 | 17.54 | +| qft_n160 | 824.704 | 824.437 | 190.68 | 190.31 | +| qft_n18 | 181.577 | 181.105 | 23.35 | 23.23 | +| ising_n26 | 20.200 | 20.232 | 20.41 | 20.41 | + +The sibling-loop case saves 1.69 MiB of peak RSS (2.8%). Other memory changes +are small. Conversion timing is essentially unchanged. Restoring the three range +callers changes the full QFT-18 median from 180.379 to 181.105 ms and Ising-26 +from 20.210 to 20.232 ms: no material regression in these cases. The +full-pipeline measurements do not establish a general compiler speedup. + +## Validation and artifacts + +- Full release build and CTest: 4,015 tests pass, including 2,910 MLIR tests, + with one expected `ScQDMIJobSpecificationTest.QueryJobId` skip. +- All 90 wire and 105 conversion/pipeline samples pass their output checks. +- The misplaced-yield CLI reproducer rejects cleanly, and its regression passes. +- MLIR reference generation and repository lint pass. +- Whole-file C++ lint passes for all 20 selected files, including the updated + iterator test. Final rebuilt probes reproduce the sibling-loop and both + full-pipeline output hashes and gate counts. + +No Python, wheel, sanitizer, or native Windows runs were added for this +follow-up. The preceding portable death-test fix remains in the branch. Hosted +CI must validate the published head separately. + +External artifacts are in +`/home/nvidia/.codex/experiments/core-qc-qco-combined-20261018/followup-20261019/`: +wire source and inputs, runner, manifests, raw samples, reproducer, build and +validation logs. Conversion probes and their compile/link commands are in the +sibling `measurements/` directory under the `followup-` labels. The +direct-caller variant uses the baseline versions of the three consumer files; +all other source matches the measured range variant. Final lint fixes relocate +static helpers and format initializers without changing the algorithm. No +benchmark harness or generated input is shipped. diff --git a/mlir/include/mqt/Dialect/QCO/IR/QCOInterfaces.td b/mlir/include/mqt/Dialect/QCO/IR/QCOInterfaces.td index 5933139dda..0524255d30 100644 --- a/mlir/include/mqt/Dialect/QCO/IR/QCOInterfaces.td +++ b/mlir/include/mqt/Dialect/QCO/IR/QCOInterfaces.td @@ -25,6 +25,9 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { The interface enables uniform introspection and composition capabilities across all unitary operations with value semantics. + Input and output qubit ranges correspond positionally: the i-th output + continues the i-th input wire, including for SWAP operations. + Gate parameters must be finite, including values produced at runtime. Operation verification only checks direct constants as a sanity check. }]; diff --git a/mlir/include/mqt/Dialect/QCO/Utils/WireIterator.h b/mlir/include/mqt/Dialect/QCO/Utils/WireIterator.h index 01ccce71cd..f602121fb1 100644 --- a/mlir/include/mqt/Dialect/QCO/Utils/WireIterator.h +++ b/mlir/include/mqt/Dialect/QCO/Utils/WireIterator.h @@ -22,6 +22,7 @@ namespace mlir::qco { /// /// The iterator follows the flow of a qubit through a sequence of quantum /// operations while respecting the semantics of each operation. +/// It requires a scalar QCO qubit and linear def-use chains. /// Unitary calls preserve the wire without entering the callee. Generic /// `func.call` operations end traversal; clients manage callee traversal. class [[nodiscard]] WireIterator { @@ -38,8 +39,6 @@ class [[nodiscard]] WireIterator { : op_(qubit.getDefiningOp()), qubit_(qubit) { if (op_ == nullptr || isHead(op_)) { pos_ = Position::Head; - } else if (isTail(op_)) { - pos_ = Position::Tail; } else { pos_ = Position::Between; } @@ -91,9 +90,6 @@ class [[nodiscard]] WireIterator { /// Labels the position on the wire. enum class Position : uint8_t { BeforeHead, Head, Between, Tail, PastTail }; - /// Return true, if an op doesn't return, but only consumes, a qubit value. - static bool isTail(Operation*); - /// Return true, if an op doesn't consume, but only returns, a qubit value. static bool isHead(Operation*); diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 183384e042..fe9feae5af 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -36,12 +36,12 @@ #include "mlir/IR/Types.h" #include "mlir/IR/Value.h" #include "mlir/IR/ValueRange.h" +#include "mlir/Interfaces/FunctionInterfaces.h" #include "mlir/Rewrite/PatternApplicator.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" #include "mlir/Support/WalkResult.h" #include "mlir/Transforms/CSE.h" -#include "mlir/Transforms/DialectConversion.h" #include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/RegionUtils.h" @@ -124,18 +124,9 @@ struct LoweringState { /// Local physical tensors released at function return, in allocation order. DenseMap> localReferenceRegisters; - /// Map from an operation to its used QC qubits inside its regions - DenseMap> regionQubitMap; - - /// Map from an operation to the registers used inside its regions. - DenseMap> regionRegisterMap; - - /// Original QC value order for already-converted structured operations. + /// Original QC capture order for source and converted structured operations. DenseMap structuredValues; - /// Qubit keys yielded by converted modifier regions, in yield order. - DenseMap> modifierRegionQubits; - /// The qubit allocation mode used in the module AllocationMode allocationMode = AllocationMode::Unset; @@ -159,22 +150,13 @@ template class LoweringPattern : public OpRewritePattern { public: - LoweringPattern(TypeConverter& typeConverter, MLIRContext* context, - LoweringState* state) - : OpRewritePattern(context), state_(state), - typeConverter_(&typeConverter) {} + LoweringPattern(MLIRContext* context, LoweringState* state) + : OpRewritePattern(context), state_(state) {} [[nodiscard]] LoweringState& getState() const { return *state_; } - [[nodiscard]] TypeConverter* getQCToQCOTypeConverter() const { - return typeConverter_; - } private: LoweringState* state_; - /// Quantum operands use region-local state, not automatic materializations. - /// - /// The converter is needed only for signatures and explicit result types. - TypeConverter* typeConverter_; }; } // namespace @@ -380,17 +362,9 @@ static void commitQubits(LoweringState& state, Operation* anchor, /// Resolves all structured QC state to QCO and QTensor values. [[nodiscard]] static SmallVector resolveAllValues(LoweringState& state, Operation* anchor) { - ArrayRef registers; - ArrayRef qcQubits; auto* owner = structuredValueOwner(anchor); - if (const auto it = state.structuredValues.find(owner); - it != state.structuredValues.end()) { - registers = it->second.registers; - qcQubits = it->second.qubits; - } else { - registers = state.regionRegisterMap[owner].getArrayRef(); - qcQubits = state.regionQubitMap[owner].getArrayRef(); - } + const auto& [qcQubits, registers] = + state.structuredValues.find(owner)->second; SmallVector qcoTargets; qcoTargets.reserve(registers.size() + qcQubits.size()); @@ -798,10 +772,8 @@ static void collectStructuredCaptures(Operation* root, LoweringState& state) { SetVector captures; getUsedValuesDefinedAbove(operation->getRegions(), captures); - auto& qubits = state.regionQubitMap[operation]; - auto& registers = state.regionRegisterMap[operation]; - qubits.clear(); - registers.clear(); + SmallVector qubits; + SetVector registers; for (auto value : captures) { if (const auto access = state.registerAccesses.find(value); access != state.registerAccesses.end()) { @@ -809,13 +781,19 @@ static void collectStructuredCaptures(Operation* root, LoweringState& state) { continue; } if (isa(value.getType())) { - qubits.insert(value); + qubits.push_back(value); continue; } if (isQubitMemrefType(value.getType())) { registers.insert(lookupRegisterId(state, value)); } } + if (!qubits.empty() || !registers.empty()) { + state.structuredValues[operation] = { + .qubits = std::move(qubits), + .registers = registers.takeVector(), + }; + } }); } @@ -834,12 +812,28 @@ static void moveModifierRegion(Region& source, Region& destination, static void initializeModifierRegionState(Operation* op, LoweringState& state) { auto& region = op->getRegion(0); auto args = region.front().getArguments(); - // No old->new alias is needed: old and new values are identical. - state.modifierRegionQubits[®ion] = - SmallVector(args.begin(), args.end()); seedRegionMappings(state, region, args, {}, args, {}); } +/// Returns whether the type changes when lowering QC references to QCO values. +[[nodiscard]] static bool isQCType(Type type) { + auto memref = dyn_cast(type); + return isa(type) || + (memref && isa(memref.getElementType())); +} + +[[nodiscard]] static Type convertQCType(Type type) { + if (isa(type)) { + return qco::QubitType::get(type.getContext()); + } + if (auto memref = dyn_cast(type); + memref && isa(memref.getElementType())) { + return RankedTensorType::get(memref.getShape(), + qco::QubitType::get(type.getContext())); + } + return type; +} + namespace { /// Converts func.return and sinks remaining live qubits. @@ -909,50 +903,18 @@ struct ConvertFuncReturnOp final : LoweringPattern { } }; -/// Type converter for QC-to-QCO conversion -/// -/// Handles type conversion between the QC and QCO dialects. -/// The primary conversion is from !qc.qubit to !qco.qubit, which -/// represents the semantic shift from reference types to value types. -/// -/// Other types (integers, booleans, etc.) pass through unchanged via -/// the identity conversion. -class QCToQCOTypeConverter final : public TypeConverter { -public: - explicit QCToQCOTypeConverter(MLIRContext* ctx) { - // Identity conversion for all types by default - addConversion([](Type type) { return type; }); - - // Convert QC qubit references to QCO qubit values - addConversion([ctx](qc::QubitType /*type*/) -> Type { - return qco::QubitType::get(ctx); - }); - addConversion([ctx](MemRefType type) -> Type { - if (isa(type.getElementType())) { - return RankedTensorType::get(type.getShape(), qco::QubitType::get(ctx)); - } - return type; - }); - } -}; - struct ConvertFuncOp final : LoweringPattern { using LoweringPattern::LoweringPattern; LogicalResult matchAndRewrite(func::FuncOp op, PatternRewriter& rewriter) const override { - if (getQCToQCOTypeConverter()->isSignatureLegal(op.getFunctionType())) { + if (llvm::none_of(op.getArgumentTypes(), isQCType) && + llvm::none_of(op.getResultTypes(), isQCType)) { return failure(); } - SmallVector inputs; - SmallVector results; - if (failed(getQCToQCOTypeConverter()->convertTypes(op.getArgumentTypes(), - inputs)) || - failed(getQCToQCOTypeConverter()->convertTypes(op.getResultTypes(), - results))) { - return failure(); - } + auto inputs = llvm::map_to_vector(op.getArgumentTypes(), convertQCType); + auto results = llvm::map_to_vector(op.getResultTypes(), convertQCType); SmallVector qubitArguments; SmallVector resultAttrs; @@ -1016,11 +978,7 @@ struct ConvertFuncCallOp final : LoweringPattern { qcQubits.emplace_back(operand); } } - SmallVector resultTypes; - if (failed(getQCToQCOTypeConverter()->convertTypes(op.getResultTypes(), - resultTypes))) { - return failure(); - } + auto resultTypes = llvm::map_to_vector(op.getResultTypes(), convertQCType); auto materialized = materializeQubits(state, op, qcQubits, rewriter); SmallVector operands(op.getOperands()); SmallVector quantumArguments; @@ -1533,19 +1491,18 @@ struct ConvertQCYieldOp final : LoweringPattern { LogicalResult matchAndRewrite(qc::YieldOp op, PatternRewriter& rewriter) const override { + if (!isa(op->getParentOp())) { + return rewriter.notifyMatchFailure(op, + "expected a converted QC modifier"); + } auto& state = getState(); auto* operation = op.getOperation(); auto* region = op->getParentRegion(); - const auto frame = state.modifierRegionQubits.find(region); - if (frame == state.modifierRegionQubits.end()) { - return rewriter.notifyMatchFailure(op, "missing modifier region state"); - } - - auto targets = resolveMappedQubits(state, operation, frame->second); + auto targets = + resolveMappedQubits(state, operation, region->front().getArguments()); rewriter.replaceOpWithNewOp(op, targets); state.qubitMap.erase(region); state.tensorMap.erase(region); - state.modifierRegionQubits.erase(frame); return success(); } }; @@ -1556,14 +1513,14 @@ struct ConvertQCYieldOp final : LoweringPattern { /// @par Example: /// ```mlir /// scf.for %iv = %lb to %ub step %step { -/// %q0 = qc.load %memref[%iv] : !memref<3x!qc.qubit> +/// %q0 = memref.load %memref[%iv] : memref<3x!qc.qubit> /// qc.h %q0 : !qc.qubit /// } /// ``` /// is converted to /// ```mlir /// %targets_out = scf.for %iv = %lb to %ub step %step iter_args(%arg0 = -/// %qtensor) -> (tensor<3x!qco.qubit) { +/// %qtensor) -> (tensor<3x!qco.qubit>) { /// %t0, %q0 = qtensor.extract %arg0[%iv] : tensor<3x!qco.qubit> /// %q1 = qco.h %q0 : !qco.qubit /// %t1 = qtensor.insert %q1 into %t0[%iv] : tensor<3x!qco.qubit> @@ -1577,10 +1534,9 @@ struct ConvertSCFForOp final : LoweringPattern { PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); - auto& registerMap = state.regionRegisterMap[op]; - auto& qubitMap = state.regionQubitMap[op]; - const auto numRegisters = registerMap.size(); - const auto numQubits = qubitMap.size(); + auto& [qubits, registers] = state.structuredValues.find(op)->second; + const auto numRegisters = registers.size(); + const auto numQubits = qubits.size(); auto qcoTargets = resolveAllValues(state, operation); const auto numOriginalResults = op.getNumResults(); @@ -1592,11 +1548,11 @@ struct ConvertSCFForOp final : LoweringPattern { op.getUpperBound(), op.getStep(), initArgs); newForOp->setDiscardableAttrs(op->getDiscardableAttrDictionary()); - assignMappedTensors(state, op.getOperation(), registerMap, + assignMappedTensors(state, op.getOperation(), registers, newForOp.getResults() .drop_front(numOriginalResults) .take_front(numRegisters)); - assignMappedQubits(state, op.getOperation(), qubitMap, + assignMappedQubits(state, op.getOperation(), qubits, newForOp->getResults().take_back(numQubits)); // Move the contents from the old block into the new block @@ -1609,17 +1565,15 @@ struct ConvertSCFForOp final : LoweringPattern { rewriter.replaceAllUsesWith(oldArgument, newArgument); } - auto& [qubits, registers] = state.structuredValues[newForOp]; - qubits = qubitMap.takeVector(); - registers = registerMap.takeVector(); - state.regionQubitMap.erase(op); - state.regionRegisterMap.erase(op); seedRegionMappings(state, newForOp.getRegion(), qubits, registers, dstBlock.getArguments().take_back(numQubits), dstBlock.getArguments() .drop_front(1 + numOriginalResults) .take_front(numRegisters)); + auto values = std::move(state.structuredValues.find(op)->second); + state.structuredValues.erase(op); + state.structuredValues.try_emplace(newForOp, std::move(values)); rewriter.replaceOp(op, newForOp.getResults().take_front(numOriginalResults)); @@ -1643,7 +1597,7 @@ struct ConvertSCFForOp final : LoweringPattern { /// is converted to /// ```mlir /// %targets_out = scf.while (%arg0 = %q0) : (!qco.qubit) -> !qco.qubit { -/// %q1 = qco.measure %arg0 : !qco.qubit +/// %q1, %cond = qco.measure %arg0 : !qco.qubit /// scf.condition(%cond) %q1 : !qco.qubit /// } do { /// ^bb0(%arg0: !qco.qubit): @@ -1658,10 +1612,9 @@ struct ConvertSCFWhileOp final : LoweringPattern { PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); - auto& registerMap = state.regionRegisterMap[op]; - auto& qubitMap = state.regionQubitMap[op]; - const auto numRegisters = registerMap.size(); - const auto numQubits = qubitMap.size(); + auto& [qubits, registers] = state.structuredValues.find(op)->second; + const auto numRegisters = registers.size(); + const auto numQubits = qubits.size(); auto qcoTargets = resolveAllValues(state, operation); const auto numOriginalInits = op.getInits().size(); @@ -1676,11 +1629,11 @@ struct ConvertSCFWhileOp final : LoweringPattern { auto newWhileOp = scf::WhileOp::create(rewriter, op.getLoc(), resultTypes, initArgs); - assignMappedTensors(state, op.getOperation(), registerMap, + assignMappedTensors(state, op.getOperation(), registers, newWhileOp.getResults() .drop_front(numOriginalResults) .take_front(numRegisters)); - assignMappedQubits(state, op.getOperation(), qubitMap, + assignMappedQubits(state, op.getOperation(), qubits, newWhileOp->getResults().take_back(numQubits)); auto& newBeforeRegion = newWhileOp.getBefore(); @@ -1714,11 +1667,6 @@ struct ConvertSCFWhileOp final : LoweringPattern { rewriter.replaceAllUsesWith(oldArgument, newArgument); } - auto& [qubits, registers] = state.structuredValues[newWhileOp]; - qubits = qubitMap.takeVector(); - registers = registerMap.takeVector(); - state.regionQubitMap.erase(op); - state.regionRegisterMap.erase(op); seedRegionMappings(state, newWhileOp.getBefore(), qubits, registers, newBeforeBlock->getArguments().take_back(numQubits), newBeforeBlock->getArguments() @@ -1730,6 +1678,9 @@ struct ConvertSCFWhileOp final : LoweringPattern { .drop_front(numOriginalResults) .take_front(numRegisters)); + auto values = std::move(state.structuredValues.find(op)->second); + state.structuredValues.erase(op); + state.structuredValues.try_emplace(newWhileOp, std::move(values)); rewriter.replaceOp(op, newWhileOp.getResults().take_front(numOriginalResults)); return success(); @@ -1760,19 +1711,18 @@ struct ConvertSCFIfOp final : LoweringPattern { PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); - auto& registerMap = state.regionRegisterMap[op]; - auto& qubitMap = state.regionQubitMap[op]; - const auto numRegisters = registerMap.size(); - const auto numQubits = qubitMap.size(); + auto& [qubits, registers] = state.structuredValues.find(op)->second; + const auto numRegisters = registers.size(); + const auto numQubits = qubits.size(); auto qcoTargets = resolveAllValues(state, operation); auto newIfOp = IfOp::create(rewriter, op.getLoc(), op.getResultTypes(), ValueRange(qcoTargets).getTypes(), op.getCondition(), qcoTargets); - assignMappedTensors(state, op.getOperation(), registerMap, + assignMappedTensors(state, op.getOperation(), registers, newIfOp.getLinearResults().take_front(numRegisters)); - assignMappedQubits(state, op.getOperation(), qubitMap, + assignMappedQubits(state, op.getOperation(), qubits, newIfOp.getLinearResults().take_back(numQubits)); auto& thenRegion = newIfOp.getThenRegion(); @@ -1791,12 +1741,6 @@ struct ConvertSCFIfOp final : LoweringPattern { thenBlock->getOperations().splice( thenBlock->end(), op.getThenRegion().front().getOperations()); - auto& [qubits, registers] = state.structuredValues[newIfOp]; - qubits = qubitMap.takeVector(); - registers = registerMap.takeVector(); - state.regionQubitMap.erase(op); - state.regionRegisterMap.erase(op); - if (!op.getElseRegion().empty()) { elseBlock->getOperations().splice( elseBlock->end(), op.getElseRegion().front().getOperations()); @@ -1814,6 +1758,9 @@ struct ConvertSCFIfOp final : LoweringPattern { thenBlock->getArguments().take_back(numQubits), thenBlock->getArguments().take_front(numRegisters)); + auto values = std::move(state.structuredValues.find(op)->second); + state.structuredValues.erase(op); + state.structuredValues.try_emplace(newIfOp, std::move(values)); rewriter.replaceOp(op, newIfOp.getClassicalResults()); return success(); } @@ -1850,10 +1797,9 @@ struct ConvertSCFIndexSwitchOp final : LoweringPattern { PatternRewriter& rewriter) const override { auto& state = getState(); auto* operation = op.getOperation(); - auto& registerMap = state.regionRegisterMap[op]; - auto& qubitMap = state.regionQubitMap[op]; - const auto numRegisters = registerMap.size(); - const auto numQubits = qubitMap.size(); + auto& [qubits, registers] = state.structuredValues.find(op)->second; + const auto numRegisters = registers.size(); + const auto numQubits = qubits.size(); const auto targets = resolveAllValues(state, operation); const auto linearResultTypes = ValueRange(targets).getTypes(); @@ -1863,17 +1809,11 @@ struct ConvertSCFIndexSwitchOp final : LoweringPattern { rewriter, op.getLoc(), op.getResultTypes(), linearResultTypes, op.getArg(), op.getCases(), targets, op.getNumCases()); - assignMappedTensors(state, op.getOperation(), registerMap, + assignMappedTensors(state, op.getOperation(), registers, newOp.getLinearResults().take_front(numRegisters)); - assignMappedQubits(state, op.getOperation(), qubitMap, + assignMappedQubits(state, op.getOperation(), qubits, newOp.getLinearResults().take_back(numQubits)); - auto& [qubits, registers] = state.structuredValues[newOp]; - qubits = qubitMap.takeVector(); - registers = registerMap.takeVector(); - state.regionQubitMap.erase(op); - state.regionRegisterMap.erase(op); - const auto newCaseRegions = newOp.getCaseRegions(); const auto oldCaseRegions = op.getCaseRegions(); const auto buildRegion = [&](Region& oldRegion, Region& newRegion) { @@ -1894,6 +1834,9 @@ struct ConvertSCFIndexSwitchOp final : LoweringPattern { } buildRegion(op.getDefaultRegion(), newOp.getDefaultRegion()); + auto values = std::move(state.structuredValues.find(op)->second); + state.structuredValues.erase(op); + state.structuredValues.try_emplace(newOp, std::move(values)); rewriter.replaceOp(op, newOp.getClassicalResults()); return success(); } @@ -1991,7 +1934,6 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { auto* ctx = moduleOp.getContext(); auto* qcDialect = ctx->getLoadedDialect(); auto* memrefDialect = ctx->getLoadedDialect(); - QCToQCOTypeConverter converter(ctx); RewritePatternSet patterns(ctx); patterns.add< ConvertQCDeallocOp, ConvertQCMeasureOp, ConvertQCResetOp, @@ -2000,14 +1942,13 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { ConvertSCFIfOp, ConvertSCFIndexSwitchOp, ConvertSCFYieldOp, ConvertSCFConditionOp, ConvertFuncReturnOp, ConvertMemRefAllocOp, ConvertMemRefAllocaOp, ConvertMemRefStoreOp, ConvertMemRefDeallocOp, - ConvertFuncOp, ConvertFuncCallOp, ConvertQCCallOp>(converter, ctx, - &state); - patterns.add>( - converter, ctx, &state); + ConvertFuncOp, ConvertFuncCallOp, ConvertQCCallOp>(ctx, &state); + patterns.add>(ctx, + &state); #define MQT_GATE(KEY, NAME, GETTER, TARGETS, PARAMS, SUFFIX, CTL_SUFFIX) \ patterns.add< \ ConvertQCGateToQCO>( \ - converter, ctx, &state); + ctx, &state); #include "mqt/Conversion/GateTable.def" FrozenRewritePatternSet frozen(std::move(patterns)); PatternApplicator applicator(frozen); @@ -2045,14 +1986,16 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { } bool required = operation->getDialect() == qcDialect; if (auto function = dyn_cast(operation)) { - required = !converter.isSignatureLegal(function.getFunctionType()); + required = llvm::any_of(function.getArgumentTypes(), isQCType) || + llvm::any_of(function.getResultTypes(), isQCType); if (required && !function.isExternal()) { sourceArgumentBlocks.emplace_back(&function.getBody().front(), function.getNumArguments()); } } if (isa(operation)) { - required = !converter.isLegal(operation); + required = llvm::any_of(operation->getOperandTypes(), isQCType) || + llvm::any_of(operation->getResultTypes(), isQCType); if (required) { sources.push_back(operation); } @@ -2060,7 +2003,7 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { if (isa(operation)) { auto* region = operation->getParentRegion(); auto it = state.qubitMap.find(region); - required = !converter.isLegal(operation) || + required = llvm::any_of(operation->getOperandTypes(), isQCType) || state.functionQubitArguments.contains( operation->getParentOfType()) || state.localReferenceRegisters.contains(region) || @@ -2085,8 +2028,7 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { } if (isa( operation)) { - required = !state.regionQubitMap[operation].empty() || - !state.regionRegisterMap[operation].empty(); + required = state.structuredValues.contains(operation); } if (isa(operation)) { required = state.structuredValues.contains(operation->getParentOp()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 10e4e368ca..3b714a439a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -1823,15 +1823,13 @@ struct MappingPass : impl::MappingPassBase { DenseMap& cache) { SmallVector measurements; bool needsRouting = false; - Operation* op = measurement; - while (true) { + for (auto* op : WireRange(measurement.getQubitOut())) { if (const auto cached = cache.find(op); cached != cache.end()) { needsRouting = cached->second; break; } if (auto next = dyn_cast(op)) { measurements.push_back(next); - op = *next.getQubitOut().user_begin(); continue; } // Validated tensor insertion tails become sinks during placement. diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 4fc1630a42..e68c46f675 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -16,6 +16,7 @@ #include "mqt/Dialect/QCO/Transforms/NativeSynthesis/SingleQubitFusion.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" +#include "mqt/Dialect/QCO/Utils/WireIterator.h" #include "mqt/Dialect/QTensor/IR/QTensorDialect.h" // IWYU pragma: keep (Passes.h.inc) #include "mlir/Dialect/Arith/IR/Arith.h" // IWYU pragma: keep (Passes.h.inc) @@ -80,13 +81,13 @@ scanFusableRun(UnitaryOpInterface head, const Matrix2x2& headMatrix, const decomposition::SingleQubitBasis basis, const CompilerTarget* target) { FusableRunScan scan; - for (auto member = head; member; - member = dyn_cast( - *member.getOutputQubit(0).user_begin())) { - auto* op = member.getOperation(); - const auto matrix = member.getOperation() == head.getOperation() - ? std::optional{headMatrix} - : getRunMemberMatrix(member); + for (auto* op : WireRange(head.getOutputQubit(0))) { + auto member = dyn_cast(op); + if (!member) { + break; + } + const auto matrix = op == head.getOperation() ? std::optional{headMatrix} + : getRunMemberMatrix(member); if (!matrix) { break; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index b87a407b15..9b6d84499c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -19,6 +19,7 @@ #include "mqt/Dialect/QCO/Transforms/NativeSynthesis/SingleQubitFusion.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" +#include "mqt/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Math/IR/Math.h" @@ -502,14 +503,14 @@ struct MergeSingleQubitRotationGatesPattern final /// @return The chain of operations in circuit order (first applied to last) static SmallVector collectChain(UnitaryOpInterface start) { - SmallVector chain{start}; - while (true) { - auto* next = *chain.back().getOutputQubit(0).user_begin(); - if (!isMergeable(next)) { - return chain; + SmallVector chain; + for (auto* op : WireRange(start.getOutputQubit(0))) { + if (!isMergeable(op)) { + break; } - chain.emplace_back(cast(next)); + chain.emplace_back(cast(op)); } + return chain; } static bool hasDynamicParameter(ArrayRef chain) { diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index c7bce4f2de..8dc2480c35 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -16,7 +16,6 @@ #include "mqt/Dialect/QCO/QCOUtils.h" #include "mqt/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/Operation.h" #include "mlir/IR/Types.h" @@ -33,13 +32,6 @@ namespace mlir::qco { -bool WireIterator::isTail(Operation* op) { - // `qtensor.from_elements` takes qubits into a tensor just like - // `qtensor.insert` does, so a wire reaching either of them ends there. - return isa(op); -} - bool WireIterator::isHead(Operation* op) { return isa(op); } @@ -80,33 +72,27 @@ void WireIterator::forward() { // Find the user-operation of the qubit SSA value. assert(qubit_.hasOneUse() && "expected linear semantics"); - op_ = *qubit_.user_begin(); - - if (isTail(op_)) { - pos_ = Position::Tail; - return; - } + auto& use = *qubit_.use_begin(); + op_ = use.getOwner(); // Find the output from the input qubit SSA value. pos_ = Position::Between; TypeSwitch(op_) - .Case( - [&](UnitaryOpInterface op) { qubit_ = op.getOutputForInput(qubit_); }) + .Case([&](UnitaryOpInterface op) { + const auto index = + use.getOperandNumber() - op.getInputQubits().getBeginOperandIndex(); + qubit_ = op.getOutputQubit(index); + }) .Case([&](MeasureOp op) { qubit_ = op.getQubitOut(); }) .Case([&](ResetOp op) { qubit_ = op.getQubitOut(); }) - .Case([&](scf::ForOp op) { - qubit_ = op.getTiedLoopResult(qubit_.use_begin().getOperand()); - }) + .Case([&](scf::ForOp op) { qubit_ = op.getTiedLoopResult(&use); }) .Case([&](scf::WhileOp op) { - qubit_ = getCorrespondingQuantumValue( - op.getInits(), op.getResults(), - qubit_.use_begin()->getOperandNumber()); - }) - .Case([&](IfOp op) { qubit_ = op.getTiedResult(&(*qubit_.use_begin())); }) - .Case([&](IndexSwitchOp op) { - qubit_ = op.getTiedResult(&(*qubit_.use_begin())); + qubit_ = getCorrespondingQuantumValue(op.getInits(), op.getResults(), + use.getOperandNumber()); }) + .Case([&](IfOp op) { qubit_ = op.getTiedResult(&use); }) + .Case([&](IndexSwitchOp op) { qubit_ = op.getTiedResult(&use); }) .Default([&](Operation*) { pos_ = Position::Tail; }); } diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index e079d11e51..03dbc7f0a1 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -316,6 +316,17 @@ module { EXPECT_TRUE(destination.getBody().empty()); } +TEST_F(QCToQCORegressionTest, RejectsYieldOutsideModifier) { + auto moduleOp = parseSourceString( + "module { func.func @main(%x: i64) { qc.yield } }", &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + ScopedDiagnosticHandler handler( + &context, [](Diagnostic& /*diagnostic*/) { return success(); }); + EXPECT_TRUE(failed(runQCToQCOConversion(*moduleOp))); +} + TEST_F(QCToQCORegressionTest, PreservesForResultsWithQuantumState) { constexpr llvm::StringLiteral source = R"mlir( module { diff --git a/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp b/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp index d992300442..0afbfb786c 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp @@ -355,22 +355,35 @@ TEST_F(WireIteratorFixture, UnitaryCallContinuesWire) { QCOProgramBuilder builder(context.get()); builder.initialize(); auto flip = builder.createUnitaryFunction( - "flip", TypeRange{QubitType::get(context.get())}, + "flip", + TypeRange{ + builder.getF64Type(), + QubitType::get(context.get()), + QubitType::get(context.get()), + }, [&](ValueRange arguments) { - return SmallVector{builder.x(arguments[0])}; + return SmallVector{ + builder.rx(arguments[0], arguments[1]), + builder.x(arguments[2]), + }; }); - Value input = builder.allocQubit(); - Value output = builder.call(flip, input).front(); - builder.sink(output); + SmallVector inputs{builder.allocQubit(), builder.allocQubit()}; + auto outputs = + builder.call(flip, {builder.floatConstant(0.5), inputs[0], inputs[1]}); + for (auto output : outputs) { + builder.sink(output); + } auto moduleOp = builder.finalize(); ASSERT_TRUE(moduleOp); - WireIterator iterator(input); - ++iterator; - EXPECT_TRUE(isa(iterator.operation())); - EXPECT_EQ(iterator.qubit(), output); - --iterator; - EXPECT_EQ(iterator.qubit(), input); + for (size_t i = 0; i < inputs.size(); ++i) { + WireIterator iterator(inputs[i]); + ++iterator; + EXPECT_TRUE(isa(iterator.operation())); + EXPECT_EQ(iterator.qubit(), outputs[i]); + --iterator; + EXPECT_EQ(iterator.qubit(), inputs[i]); + } } TEST_F(WireIteratorFixture, GenericCallIsWireBoundary) { From ac30ce400cfa3f7649b80b03bc2b452cee00899c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 19 Oct 2026 11:41:04 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9A=A1=20Avoid=20repeated=20QC=20convers?= =?UTF-8?q?ion=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Erase converted source calls once their classical results are replaced, while retaining calls whose QC results still identify mapped state. Reuse register analysis when normalization leaves the IR unchanged, and let full CSE report constant merging reliably before deciding whether to rebuild that analysis. Avoid copied operand lists and single-qubit alias bookkeeping. Remove the redundant symbol lookup from wire-origin collection under its verified-input contract. Extend existing tests for chained classical calls and aliases exposed by folding or CSE without increasing the test count. Validate 4015 native tests plus one existing skip, all 178 conversion tests after the final fixture simplification, repository lint, whole-file C++ lint, and an independent Ponytail review. Nine paired runs across 115 built-in inputs preserve identical output. Cases taking at least 1 ms improve by 7-15%; the largest distillation case uses 6.4 MiB less peak RSS. Measurements use CPU affinity on a shared DGX Spark, not reserved cores. Assisted-by: GPT-6 via Codex --- mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 58 +++++++++------ mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp | 4 +- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 70 +++++++++++-------- 3 files changed, 79 insertions(+), 53 deletions(-) diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index fe9feae5af..63ae28a25d 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -392,18 +392,25 @@ static void commitQubits(LoweringState& state, Operation* anchor, } /// Hoist static qubit references and let CSE coalesce them. -[[nodiscard]] static LogicalResult normalizeStaticQubits(ModuleOp moduleOp) { +[[nodiscard]] static LogicalResult normalizeStaticQubits(ModuleOp moduleOp, + bool& changed) { RewritePatternSet patterns(moduleOp.getContext()); qc::StaticOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); // Normalization needs folding and static-qubit hoisting, not CFG cleanup. GreedyRewriteConfig config; config.setRegionSimplificationLevel(GreedySimplifyRegionLevel::Disabled); - if (failed(applyPatternsGreedily(moduleOp, std::move(patterns), config))) { + // Full CSE must report constant replacements before reusing register + // analysis. + config.enableConstantCSE(false); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns), config, + &changed))) { return failure(); } IRRewriter rewriter(moduleOp.getContext()); DominanceInfo dominance(moduleOp); - eliminateCommonSubExpressions(rewriter, dominance, moduleOp); + bool cseChanged = false; + eliminateCommonSubExpressions(rewriter, dominance, moduleOp, &cseChanged); + changed |= cseChanged; return success(); } @@ -676,18 +683,12 @@ collectRegisterAccesses(Operation* root, LoweringState& state) { } const auto distinctResult = root->walk([&](Operation* operation) { - SmallVector operationQubits; + ValueRange operands; + DenseSet borrowedRegisters; if (auto unitary = dyn_cast(operation)) { - llvm::append_range(operationQubits, unitary.getQubits()); + operands = unitary.getQubits(); } else if (auto call = dyn_cast(operation)) { - for (auto operand : call.getOperands()) { - if (isa(operand.getType())) { - operationQubits.emplace_back(operand); - } - } - } - DenseSet borrowedRegisters; - if (auto call = dyn_cast(operation)) { + operands = call.getOperands(); for (auto operand : call.getOperands()) { if (isQubitMemrefType(operand.getType()) && !borrowedRegisters.insert(lookupRegisterId(state, operand)) @@ -698,9 +699,16 @@ collectRegisterAccesses(Operation* root, LoweringState& state) { } } + if (operands.size() < 2 && borrowedRegisters.empty()) { + return WalkResult::advance(); + } + llvm::SmallDenseSet qubits; DenseMap registerIndices; - for (auto qubit : operationQubits) { + for (auto qubit : operands) { + if (!isa(qubit.getType())) { + continue; + } if (!qubits.insert(qubit).second) { operation->emitOpError("requires distinct qubit operands"); return WalkResult::interrupt(); @@ -1031,6 +1039,9 @@ struct ConvertFuncCallOp final : LoweringPattern { rewriter.replaceAllUsesWith(source, result); } } + if (llvm::none_of(op.getResultTypes(), isQCType)) { + rewriter.eraseOp(op); + } return success(); } }; @@ -1924,11 +1935,16 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { const bool needsNormalization = !state.registerIds.empty() || !staticsAlreadyNormalized(moduleOp); if (needsNormalization) { - state = LoweringState(); - if (failed(normalizeStaticQubits(moduleOp)) || - failed(collectRegisterAccesses(moduleOp, state))) { + bool changed = false; + if (failed(normalizeStaticQubits(moduleOp, changed))) { return failure(); } + if (changed) { + state = LoweringState(); + if (failed(collectRegisterAccesses(moduleOp, state))) { + return failure(); + } + } } collectStructuredCaptures(moduleOp, state); auto* ctx = moduleOp.getContext(); @@ -1994,9 +2010,11 @@ static LogicalResult lowerToQCO(ModuleOp moduleOp) { } } if (isa(operation)) { - required = llvm::any_of(operation->getOperandTypes(), isQCType) || - llvm::any_of(operation->getResultTypes(), isQCType); - if (required) { + const bool hasQCResults = + llvm::any_of(operation->getResultTypes(), isQCType); + required = + hasQCResults || llvm::any_of(operation->getOperandTypes(), isQCType); + if (hasQCResults) { sources.push_back(operation); } } diff --git a/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp b/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp index 81e99292d2..6ff05b86d0 100644 --- a/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp +++ b/mlir/lib/Dialect/QCO/Utils/FunctionUtils.cpp @@ -307,9 +307,7 @@ LogicalResult mlir::qco::collectWireOrigins(Operation* root, origins[insert.getResult()] = origin(insert.getDest()); } else if (auto call = dyn_cast(op)) { auto arguments = getQuantumArgumentIndices(call.getOperandTypes()); - if (call.getNumResults() >= arguments.size() && - SymbolTable::lookupNearestSymbolFrom( - call, call.getCalleeAttr())) { + if (call.getNumResults() >= arguments.size()) { for (auto [argument, result] : llvm::zip_equal( arguments, call.getResults().take_back(arguments.size()))) { auto input = call.getOperand(argument); diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 03dbc7f0a1..0d6325ba87 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -800,6 +800,8 @@ module { %value = arith.constant 42 : i64 %result = func.call @reset(%true, %q, %value) : (i1, !qc.qubit, i64) -> i1 + %next = func.call @reset(%result, %q, %value) + : (i1, !qc.qubit, i64) -> i1 memref.dealloc %reg : memref<1x!qc.qubit> return } @@ -812,13 +814,14 @@ module { ASSERT_TRUE(succeeded(runQCToQCOConversion(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); EXPECT_TRUE(succeeded(qco::verifyLinearity(*moduleOp))); - auto call = *mlir::mqt::getEntryPoint(*moduleOp) - .getBody() - .getOps() - .begin(); + auto calls = llvm::to_vector( + mlir::mqt::getEntryPoint(*moduleOp).getOps()); + ASSERT_EQ(calls.size(), 2U); + auto call = calls.front(); ASSERT_EQ(call.getNumOperands(), 3U); EXPECT_TRUE(isa(call.getOperand(1).getType())); ASSERT_EQ(call.getNumResults(), 2U); + EXPECT_EQ(calls.back().getOperand(0), call.getResult(0)); EXPECT_TRUE(isa(call.getResult(1).getType())); EXPECT_TRUE(call.getOperand(1).getDefiningOp()); ASSERT_TRUE(call.getResult(1).hasOneUse()); @@ -1617,33 +1620,40 @@ module { EXPECT_TRUE(sawExpectedDiagnostic); } -TEST_F(QCToQCORegressionTest, RejectsRegisterAliasingExposedByIndexFolding) { - auto moduleOp = parseSourceString(R"mlir( - module { - func.func @main() attributes {mqt.entry_point} { - %reg = memref.alloc() : memref<2x!qc.qubit> - %zero = arith.constant 0 : index - %one = arith.constant 1 : index - %index = arith.addi %zero, %one : index - %q0 = memref.load %reg[%index] : memref<2x!qc.qubit> - %q1 = memref.load %reg[%one] : memref<2x!qc.qubit> - qc.swap %q0, %q1 : !qc.qubit, !qc.qubit - memref.dealloc %reg : memref<2x!qc.qubit> - return - } - })mlir", - &context); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); +TEST_F(QCToQCORegressionTest, RejectsRegisterAliasingExposedByNormalization) { + for (const auto* indices : { + "%lhs = arith.addi %one, %one : index\n" + "%rhs = arith.constant 2 : index", + "%lhs = arith.addi %i, %one : index\n" + "%rhs = arith.addi %i, %one : index", + }) { + SCOPED_TRACE(indices); + const auto source = std::string(R"mlir( + module { + func.func @main(%i: index) attributes {mqt.entry_point} { + %reg = memref.alloc() : memref<4x!qc.qubit> + %one = arith.constant 1 : index + )mlir") + indices + R"mlir( + %q0 = memref.load %reg[%lhs] : memref<4x!qc.qubit> + %q1 = memref.load %reg[%rhs] : memref<4x!qc.qubit> + qc.swap %q0, %q1 : !qc.qubit, !qc.qubit + memref.dealloc %reg : memref<4x!qc.qubit> + return + } + })mlir"; + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("distinct qubit operands"); - return success(); - }); - EXPECT_TRUE(failed(runQCToQCOConversion(*moduleOp))); - EXPECT_TRUE(sawExpectedDiagnostic); + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("distinct qubit operands"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*moduleOp))); + EXPECT_TRUE(sawExpectedDiagnostic); + } } TEST_P(QCToQCOTest, ProgramConversion) {