From f0ac7a2fb2a3c862c5b55a5073b2cad73ed3234b Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 17 Jun 2026 16:23:08 -0400 Subject: [PATCH 01/78] add pass option and functionality --- mlir/include/Quantum/Transforms/Passes.td | 8 ++++++ .../Quantum/Transforms/decompose_lowering.cpp | 25 +++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/mlir/include/Quantum/Transforms/Passes.td b/mlir/include/Quantum/Transforms/Passes.td index e8da695ae2..16740f815c 100644 --- a/mlir/include/Quantum/Transforms/Passes.td +++ b/mlir/include/Quantum/Transforms/Passes.td @@ -189,6 +189,14 @@ def GraphDecompositionPass : Pass<"graph-decomposition", "mlir::ModuleOp"> { def DecomposeLoweringPass : Pass<"decompose-lowering"> { let summary = "Replace quantum operations with compiled decomposition rules."; + + let options = [ + ListOption< + /*C++ name*/"targetRulesOption", + /*CLI name*/"target-rules", + /*Type*/"std::string", + /*Description*/"The set of decomposition rules to apply. If empty, applies all rules."> + ]; } def DisentangleCNOTPass : Pass<"disentangle-cnot"> { diff --git a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp index 522ed966b8..101f661612 100644 --- a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp +++ b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #define DEBUG_TYPE "decompose-lowering" -// When we read the decomposition rules module from file, -// StablehloDialect may not be registered from start. +#include + #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" @@ -24,14 +25,18 @@ #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/Dialect/UB/IR/UBOps.h" +#include "mlir/IR/BuiltinOps.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/IR/PatternMatch.h" #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassManager.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/WalkResult.h" #include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/Passes.h" -#include "stablehlo/dialect/StablehloOps.h" +#include "stablehlo/dialect/StablehloOps.h" // When we read the decomposition rules module from file, StablehloDialect may not be registered from start. +#include "Quantum/IR/QuantumDialect.h" #include "Quantum/IR/QuantumOps.h" #include "Quantum/Transforms/Patterns.h" @@ -96,9 +101,15 @@ struct DecomposeLoweringPass : impl::DecomposeLoweringPassBase &decompositionRegistry) + llvm::StringMap &decompositionRegistry, + llvm::StringSet<> targetRules) { module.walk([&](func::FuncOp func) { + // if targetRules is provided, only add requested rules + if (!targetRules.empty() && !targetRules.contains(func.getName())) { + return WalkResult::skip(); + } + if (StringRef targetOp = DecompUtils::getTargetGateName(func); !targetOp.empty()) { removeUnusedFuncArgs(func); if (targetOp == "MultiRZ") { @@ -183,7 +194,11 @@ struct DecomposeLoweringPass : impl::DecomposeLoweringPassBase(getOperation()); // Step 1: Discover and register all decomposition functions in the module - discoverAndRegisterDecompositions(module, decompositionRegistry); + llvm::StringSet<> targetRules; + for (auto rule : targetRulesOption) { + targetRules.insert(rule); + } + discoverAndRegisterDecompositions(module, decompositionRegistry, targetRules); if (decompositionRegistry.empty()) { return; } From 9101dd8a9b5ecb3b42fdf8fc9c96f3a09ecddbdf Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 17 Jun 2026 16:28:25 -0400 Subject: [PATCH 02/78] add test --- .../DecomposeLoweringTargetRulesTest.mlir | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir diff --git a/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir b/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir new file mode 100644 index 0000000000..70bfce70f7 --- /dev/null +++ b/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir @@ -0,0 +1,50 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// RUN: quantum-opt --pass-pipeline='builtin.module(decompose-lowering{target-rules=my_X_decomp,my_Z_decomp})' --split-input-file -verify-diagnostics %s | FileCheck %s + +// Test that decompose-lowering only applies the rules requested by the `target-rules` option when present + +module @test_module { + // CHECK: func.func private @my_X_decomp + func.func private @my_X_decomp(%q: !quantum.bit) -> !quantum.bit attributes {target_gate="X"} { + %angle = arith.constant 1.57 : f64 + %out = quantum.custom "RX"(%angle) %q : !quantum.bit + return %out : !quantum.bit + } + + func.func private @my_Y_decomp(%q: !quantum.bit) -> !quantum.bit attributes {target_gate="Y"} { + %angle = arith.constant 1.57 : f64 + %out = quantum.custom "RY"(%angle) %q : !quantum.bit + return %out : !quantum.bit + } + + // CHECK: func.func private @my_Z_decomp + func.func private @my_Z_decomp(%q: !quantum.bit) -> !quantum.bit attributes {target_gate="Z"} { + %angle = arith.constant 1.57 : f64 + %out = quantum.custom "RZ"(%angle) %q : !quantum.bit + return %out : !quantum.bit + } + + // CHECK: [[q_alloc:%.+]] = quantum.alloc_qb + // CHECK: [[x_out:%.+]] = func.call @my_X_decomp([[q_alloc]]) + // CHECK: [[y_out:%.+]] = quantum.custom "Y"() [[x_out]] + // CHECK: [[z_out:%.+]] = func.call @my_Z_decomp([[y_out]]) + // CHECK: quantum.dealloc_qb [[z_out]] + %0 = quantum.alloc_qb : !quantum.bit + %1 = quantum.custom "X"() %0 : !quantum.bit + %2 = quantum.custom "Y"() %1 : !quantum.bit + %3 = quantum.custom "Z"() %2 : !quantum.bit + quantum.dealloc_qb %3 : !quantum.bit +} From a9e06abecbdf29fc609149471599b4b178b094c7 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 23 Jun 2026 15:06:49 -0400 Subject: [PATCH 03/78] headers --- mlir/lib/Quantum/Transforms/decompose_lowering.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp index 101f661612..9954a36f2a 100644 --- a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp +++ b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#define DEBUG_TYPE "decompose-lowering" - #include #include "llvm/ADT/DenseSet.h" @@ -40,11 +37,16 @@ #include "Quantum/IR/QuantumOps.h" #include "Quantum/Transforms/Patterns.h" +#include + using namespace mlir; using namespace catalyst::quantum; +#define DEBUG_TYPE "decompose-lowering" + namespace catalyst { namespace quantum { + #define GEN_PASS_DEF_DECOMPOSELOWERINGPASS #define GEN_PASS_DECL_DECOMPOSELOWERINGPASS #include "Quantum/Transforms/Passes.h.inc" From 06b413ac6d4ed809111a7aed595874067bef676a Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 23 Jun 2026 15:13:29 -0400 Subject: [PATCH 04/78] header delimiter --- mlir/lib/Quantum/Transforms/decompose_lowering.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp index 9954a36f2a..7e4fb00f2d 100644 --- a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp +++ b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp @@ -15,6 +15,7 @@ #include #include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/AllocatorBase.h" @@ -37,8 +38,6 @@ #include "Quantum/IR/QuantumOps.h" #include "Quantum/Transforms/Patterns.h" -#include - using namespace mlir; using namespace catalyst::quantum; From 325a8148b7cc67c2ea8b4cb13971e23c11d58573 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 24 Jun 2026 16:00:33 -0400 Subject: [PATCH 05/78] custom inline in dl --- .../Transforms/DecomposeLoweringImpl.hpp | 43 +++--- .../Transforms/DecomposeLoweringPatterns.cpp | 129 +++++++++++++----- .../Quantum/Transforms/decompose_lowering.cpp | 70 ++-------- 3 files changed, 133 insertions(+), 109 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/DecomposeLoweringImpl.hpp b/mlir/lib/Quantum/Transforms/DecomposeLoweringImpl.hpp index 4fdc0d421c..ce8d37d971 100644 --- a/mlir/lib/Quantum/Transforms/DecomposeLoweringImpl.hpp +++ b/mlir/lib/Quantum/Transforms/DecomposeLoweringImpl.hpp @@ -13,15 +13,26 @@ // limitations under the License. #include // std::move_backward +#include +#include +#include +#include +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallVector.h" #include "llvm/Support/ErrorHandling.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Operation.h" #include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/Value.h" #include "mlir/IR/ValueRange.h" +#include "mlir/Support/LLVM.h" #include "Quantum/IR/QuantumOps.h" #include "Quantum/IR/QuantumTypes.h" @@ -34,8 +45,8 @@ namespace catalyst { namespace quantum { // The goal of this class is to analyze the signature of a custom operation to get the enough -// information to prepare the call operands and results for replacing the op to calling the -// decomposition function. +// information to prepare the operands and results for replacing the op with the decomposition +// function. class BaseSignatureAnalyzer { protected: bool isValid = true; @@ -177,7 +188,7 @@ class BaseSignatureAnalyzer { return latestQreg; } - // Prepare the operands for calling the decomposition function + // Prepare the operands for the decomposition function // There are two cases: // 1. The first input is a qreg, which means the decomposition function is a qreg mode function // 2. Otherwise, the decomposition function is a qubit mode function @@ -187,10 +198,10 @@ class BaseSignatureAnalyzer { // - func(qreg, param*, inWires*, inCtrlWires*?, inCtrlValues*?) -> qreg // 2. qubit mode: // - func(param*, inQubits*, inCtrlQubits*?, inCtrlValues*?) -> outQubits* - llvm::SmallVector prepareCallOperands(func::FuncOp decompFunc, PatternRewriter &rewriter, - Location loc) + llvm::SmallVector prepareOperands(func::FuncOp rule, PatternRewriter &rewriter, + Location loc) { - auto funcType = decompFunc.getFunctionType(); + auto funcType = rule.getFunctionType(); auto funcInputs = funcType.getInputs(); SmallVector funcInputsNoQreg; @@ -202,10 +213,10 @@ class BaseSignatureAnalyzer { SmallVector operands(funcInputs.size()); - auto qregIt = llvm::find_if(decompFunc.getFunctionType().getInputs(), + auto qregIt = llvm::find_if(rule.getFunctionType().getInputs(), [](mlir::Type t) { return isa(t); }); - int qregIdx = std::distance(decompFunc.getFunctionType().getInputs().begin(), qregIt); - bool hasQreg = (qregIt != decompFunc.getFunctionType().getInputs().end()); + int qregIdx = std::distance(rule.getFunctionType().getInputs().begin(), qregIt); + bool hasQreg = (qregIt != rule.getFunctionType().getInputs().end()); int operandIdx = 0; if (!signature.params.empty()) { @@ -264,22 +275,18 @@ class BaseSignatureAnalyzer { return operands; } - // Prepare the results for the call operation - SmallVector prepareCallResultForQreg(func::CallOp callOp, PatternRewriter &rewriter) + // Prepare the results produced by a qreg-mode decomposition rule + SmallVector prepareResultsForQreg(Value qreg, Location loc, PatternRewriter &rewriter) { - assert(callOp.getNumResults() == 1 && "only one qreg result for qreg mode is allowed"); - - auto qreg = callOp.getResult(0); assert(isa(qreg.getType()) && "only allow to have qreg result"); SmallVector newResults; - rewriter.setInsertionPointAfter(callOp); for (const auto &indices : {signature.outQubitIndices, signature.outCtrlQubitIndices}) { for (const auto &index : indices) { auto extractOp = quantum::ExtractOp::create( - rewriter, callOp.getLoc(), rewriter.getType(), qreg, - index.getValue(), index.getAttr()); + rewriter, loc, rewriter.getType(), qreg, index.getValue(), + index.getAttr()); newResults.emplace_back(extractOp.getResult()); } } @@ -325,7 +332,7 @@ class BaseSignatureAnalyzer { return {startIdx, paramTypeEnd}; } - // generate params for calling the decomposition function based on function type requirements + // generate params for the decomposition function based on function type requirements SmallVector generateParams(ValueRange signatureParams, ArrayRef funcParamTypes, PatternRewriter &rewriter, Location loc) { diff --git a/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp b/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp index a4f0fdc30d..4e80e49c43 100644 --- a/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp +++ b/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp @@ -12,13 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -#define DEBUG_TYPE "decompose-lowering" +#include +#include +#include +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" +#include "llvm/Support/AllocatorBase.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Operation.h" #include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/Value.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" #include "Quantum/IR/QuantumInterfaces.h" #include "Quantum/IR/QuantumOps.h" @@ -29,9 +43,44 @@ using namespace mlir; using namespace catalyst::quantum; +#define DEBUG_TYPE "decompose-lowering" + namespace catalyst { namespace quantum { +SmallVector inlineDecompositionRule(func::FuncOp rule, ValueRange operands, + PatternRewriter &rewriter) +{ + Block &body = rule.front(); + auto returnOp = cast(body.getTerminator()); + + IRMapping mapping; + mapping.map(body.getArguments(), operands); + + for (Operation &op : body.without_terminator()) { + rewriter.clone(op, mapping); + } + + SmallVector results; + for (Value operand : returnOp.getOperands()) { + results.push_back(mapping.lookupOrDefault(operand)); + } + return results; +} + +bool isInDecompRule(Operation *op) +{ + while (auto parentOp = op->getParentOp()) { + if (auto funcOp = dyn_cast(parentOp)) { + if (funcOp->hasAttr("target_gate")) { + return true; + } + } + op = parentOp; + } + return false; +} + struct DLCustomOpPattern : public OpRewritePattern { private: const llvm::StringMap &decompositionRegistry; @@ -54,18 +103,22 @@ struct DLCustomOpPattern : public OpRewritePattern { return failure(); } - // Find the corresponding decomposition function for the op + if (isInDecompRule(op)) { + return failure(); + } + + // Find the corresponding decomposition rule for the op auto it = decompositionRegistry.find(gateName); if (it == decompositionRegistry.end()) { return failure(); } - func::FuncOp decompFunc = it->second; + func::FuncOp rule = it->second; // For null decomp rules, the signature will not have any quantum values // This is a deviation from the standard decomp func signature, so we deal with it // separately - if (!llvm::any_of(llvm::concat(decompFunc.getFunctionType().getInputs(), - decompFunc.getFunctionType().getResults()), + if (!llvm::any_of(llvm::concat(rule.getFunctionType().getInputs(), + rule.getFunctionType().getResults()), [](const mlir::Type t) { return isa(t); })) { @@ -76,32 +129,32 @@ struct DLCustomOpPattern : public OpRewritePattern { return success(); } - // Here is the assumption that the decomposition function must have at least one input and + // Here is the assumption that the decomposition rule must have at least one input and // one result - assert(decompFunc.getFunctionType().getNumInputs() > 0 && + assert(rule.getFunctionType().getNumInputs() > 0 && "Decomposition function must have at least one input"); - assert(decompFunc.getFunctionType().getNumResults() >= 1 && + assert(rule.getFunctionType().getNumResults() >= 1 && "Decomposition function must have at least one result"); rewriter.setInsertionPointAfter(op); - auto enableQreg = llvm::any_of(decompFunc.getFunctionType().getInputs(), + auto enableQreg = llvm::any_of(rule.getFunctionType().getInputs(), [](mlir::Type t) { return isa(t); }); auto analyzer = CustomOpSignatureAnalyzer(op, enableQreg); assert(analyzer && "Analyzer should be valid"); - auto callOperands = analyzer.prepareCallOperands(decompFunc, rewriter, op.getLoc()); - auto callOp = - func::CallOp::create(rewriter, op.getLoc(), decompFunc.getFunctionType().getResults(), - decompFunc.getSymName(), callOperands); + auto operands = analyzer.prepareOperands(rule, rewriter, op.getLoc()); + SmallVector inlinedFunction = inlineDecompositionRule(rule, operands, rewriter); - // Replace the op with the call op and adjust the insert ops for the qreg mode - if (callOp.getNumResults() == 1 && isa(callOp.getResult(0).getType())) { - auto results = analyzer.prepareCallResultForQreg(callOp, rewriter); + // Replace the op with the inlined function and adjust the insert ops for the qreg mode + if (inlinedFunction.size() == 1 && + isa(inlinedFunction.front().getType())) { + auto results = + analyzer.prepareResultsForQreg(inlinedFunction.front(), op.getLoc(), rewriter); rewriter.replaceOp(op, results); } else { - rewriter.replaceOp(op, callOp->getResults()); + rewriter.replaceOp(op, inlinedFunction); } return success(); @@ -130,6 +183,10 @@ struct DLMultiRZOpPattern : public OpRewritePattern { return failure(); } + if (isInDecompRule(op)) { + return failure(); + } + // Find the corresponding decomposition function for the op auto numQubits = op.getInQubits().size(); auto MRZNameWithQubits = gateName + "_" + std::to_string(numQubits); @@ -165,18 +222,19 @@ struct DLMultiRZOpPattern : public OpRewritePattern { auto analyzer = MultiRZOpSignatureAnalyzer(op, enableQreg); assert(analyzer && "Analyzer should be valid"); - auto callOperands = analyzer.prepareCallOperands(decompFunc, rewriter, op.getLoc()); - auto callOp = - func::CallOp::create(rewriter, op.getLoc(), decompFunc.getFunctionType().getResults(), - decompFunc.getSymName(), callOperands); + auto operands = analyzer.prepareOperands(decompFunc, rewriter, op.getLoc()); + SmallVector inlinedFunction = + inlineDecompositionRule(decompFunc, operands, rewriter); - // Replace the op with the call op and adjust the insert ops for the qreg mode - if (callOp.getNumResults() == 1 && isa(callOp.getResult(0).getType())) { - auto results = analyzer.prepareCallResultForQreg(callOp, rewriter); + // Replace the op with the inlined function and adjust the insert ops for the qreg mode + if (inlinedFunction.size() == 1 && + isa(inlinedFunction.front().getType())) { + auto results = + analyzer.prepareResultsForQreg(inlinedFunction.front(), op.getLoc(), rewriter); rewriter.replaceOp(op, results); } else { - rewriter.replaceOp(op, callOp->getResults()); + rewriter.replaceOp(op, inlinedFunction); } return success(); @@ -205,6 +263,10 @@ struct DLPauliRotOpPattern : public OpRewritePattern { return failure(); } + if (isInDecompRule(op)) { + return failure(); + } + // Find the corresponding decomposition function for the op auto it = decompositionRegistry.find(gateName); if (it == decompositionRegistry.end()) { @@ -226,18 +288,19 @@ struct DLPauliRotOpPattern : public OpRewritePattern { auto analyzer = PauliRotOpSignatureAnalyzer(op, enableQreg); assert(analyzer && "Analyzer should be valid"); - auto callOperands = analyzer.prepareCallOperands(decompFunc, rewriter, op.getLoc()); - auto callOp = - func::CallOp::create(rewriter, op.getLoc(), decompFunc.getFunctionType().getResults(), - decompFunc.getSymName(), callOperands); + auto operands = analyzer.prepareOperands(decompFunc, rewriter, op.getLoc()); + SmallVector inlinedFunction = + inlineDecompositionRule(decompFunc, operands, rewriter); - // Replace the op with the call op and adjust the insert ops for the qreg mode - if (callOp.getNumResults() == 1 && isa(callOp.getResult(0).getType())) { - auto results = analyzer.prepareCallResultForQreg(callOp, rewriter); + // Replace the op with the inlined results and adjust the insert ops for the qreg mode + if (inlinedFunction.size() == 1 && + isa(inlinedFunction.front().getType())) { + auto results = + analyzer.prepareResultsForQreg(inlinedFunction.front(), op.getLoc(), rewriter); rewriter.replaceOp(op, results); } else { - rewriter.replaceOp(op, callOp->getResults()); + rewriter.replaceOp(op, inlinedFunction); } return success(); diff --git a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp index 7e4fb00f2d..3b7671ac71 100644 --- a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp +++ b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp @@ -12,25 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include +#include #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/AllocatorBase.h" +#include "llvm/Support/DebugLog.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/Dialect/UB/IR/UBOps.h" +#include "mlir/IR/BuiltinAttributes.h" #include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Value.h" #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassManager.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/WalkResult.h" #include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Inliner.h" #include "mlir/Transforms/Passes.h" #include "stablehlo/dialect/StablehloOps.h" // When we read the decomposition rules module from file, StablehloDialect may not be registered from start. @@ -162,33 +169,6 @@ struct DecomposeLoweringPass : impl::DecomposeLoweringPassBasegetOperandTypes())); } - // Remove unused decomposition functions: - // Since the decomposition functions are marked as public from the frontend, - // there is no way to remove them with any DCE pass automatically. - // So we need to manually remove them from the module - void removeDecompositionFunctions(ModuleOp module, - llvm::StringMap &decompositionRegistry) - { - llvm::DenseSet usedDecompositionFunctions; - - module.walk([&](func::CallOp callOp) { - if (auto targetFunc = module.lookupSymbol(callOp.getCallee())) { - if (DecompUtils::isDecompositionFunction(targetFunc)) { - usedDecompositionFunctions.insert(targetFunc); - } - } - }); - - // remove unused decomposition functions - module.walk([&](func::FuncOp func) { - if (DecompUtils::isDecompositionFunction(func) && - !usedDecompositionFunctions.contains(func)) { - func.erase(); - } - return WalkResult::skip(); - }); - } - public: void runOnOperation() final { @@ -204,44 +184,18 @@ struct DecomposeLoweringPass : impl::DecomposeLoweringPassBase Date: Wed, 24 Jun 2026 16:01:33 -0400 Subject: [PATCH 06/78] update tests --- .../DecomposeLoweringTargetRulesTest.mlir | 7 +- mlir/test/Quantum/DecomposeLoweringTest.mlir | 230 +++++++++--------- 2 files changed, 115 insertions(+), 122 deletions(-) diff --git a/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir b/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir index 70bfce70f7..6c261d07dc 100644 --- a/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir +++ b/mlir/test/Quantum/DecomposeLoweringTargetRulesTest.mlir @@ -24,6 +24,7 @@ module @test_module { return %out : !quantum.bit } + // CHECK: func.func private @my_Y_decomp func.func private @my_Y_decomp(%q: !quantum.bit) -> !quantum.bit attributes {target_gate="Y"} { %angle = arith.constant 1.57 : f64 %out = quantum.custom "RY"(%angle) %q : !quantum.bit @@ -37,10 +38,10 @@ module @test_module { return %out : !quantum.bit } - // CHECK: [[q_alloc:%.+]] = quantum.alloc_qb - // CHECK: [[x_out:%.+]] = func.call @my_X_decomp([[q_alloc]]) + // CHECK: [[q:%.+]] = quantum.alloc_qb + // CHECK: [[x_out:%.+]] = quantum.custom "RX"(%{{.+}}) [[q]] // CHECK: [[y_out:%.+]] = quantum.custom "Y"() [[x_out]] - // CHECK: [[z_out:%.+]] = func.call @my_Z_decomp([[y_out]]) + // CHECK: [[z_out:%.+]] = quantum.custom "RZ"(%{{.+}}) [[y_out]] // CHECK: quantum.dealloc_qb [[z_out]] %0 = quantum.alloc_qb : !quantum.bit %1 = quantum.custom "X"() %0 : !quantum.bit diff --git a/mlir/test/Quantum/DecomposeLoweringTest.mlir b/mlir/test/Quantum/DecomposeLoweringTest.mlir index b7abe6f203..7a6d9d3566 100644 --- a/mlir/test/Quantum/DecomposeLoweringTest.mlir +++ b/mlir/test/Quantum/DecomposeLoweringTest.mlir @@ -41,8 +41,8 @@ module @two_hadamards { return %4 : tensor<4xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @Hadamard_to_RY_decomp + // Decomposition function should be retained for future passes + // CHECK: func.func private @Hadamard_to_RY_decomp func.func private @Hadamard_to_RY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "Hadamard", llvm.linkage = #llvm.linkage} { %cst = arith.constant 3.1415926535897931 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 @@ -55,6 +55,8 @@ module @two_hadamards { // ----- // Test single Hadamard decomposition + +// CHECK-LABEL: module @single_hadamard module @single_hadamard { func.func @test_single_hadamard() -> !quantum.bit { // CHECK: [[CST_PI2:%.+]] = arith.constant 1.5707963267948966 : f64 @@ -73,8 +75,8 @@ module @single_hadamard { return %2 : !quantum.bit } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @Hadamard_to_RY_decomp + // Decomposition function should be retained for future passes + // CHECK: func.func private @Hadamard_to_RY_decomp func.func private @Hadamard_to_RY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "Hadamard", llvm.linkage = #llvm.linkage} { %cst = arith.constant 3.1415926535897931 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 @@ -85,6 +87,8 @@ module @single_hadamard { } // ----- + +// CHECK-LABEL: module @recursive module @recursive { func.func public @test_recursive() -> tensor<4xf64> attributes {quantum.node} { %0 = quantum.alloc( 2) : !quantum.reg @@ -112,15 +116,15 @@ module @recursive { return %4 : tensor<4xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @Hadamard_to_RY_decomp + // Decomposition function should be retained for future passes + // CHECK: func.func private @Hadamard_to_RY_decomp func.func private @Hadamard_to_RY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "Hadamard", llvm.linkage = #llvm.linkage} { %out_qubits_0 = quantum.custom "RZRY"() %arg0 : !quantum.bit return %out_qubits_0 : !quantum.bit } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @RZRY_decomp + // Decomposition function should be retained for future passes + // CHECK: func.func private @RZRY_decomp func.func private @RZRY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "RZRY", llvm.linkage = #llvm.linkage} { %cst = arith.constant 3.1415926535897931 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 @@ -131,6 +135,8 @@ module @recursive { } // ----- + +// CHECK-LABEL: module @recursive module @recursive { func.func public @test_recursive() -> tensor<4xf64> attributes {quantum.node} { %0 = quantum.alloc( 2) : !quantum.reg @@ -158,15 +164,15 @@ module @recursive { return %4 : tensor<4xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @Hadamard_to_RY_decomp + // Decomposition function should be retained for future passes + // CHECK: func.func private @Hadamard_to_RY_decomp func.func private @Hadamard_to_RY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "Hadamard", llvm.linkage = #llvm.linkage} { %out_qubits_0 = quantum.custom "RZRY"() %arg0 : !quantum.bit return %out_qubits_0 : !quantum.bit } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @RZRY_decomp + // Decomposition function should be retained for future passes + // CHECK: func.func private @RZRY_decomp func.func private @RZRY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "RZRY", llvm.linkage = #llvm.linkage} { %cst = arith.constant 3.1415926535897931 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 @@ -179,6 +185,8 @@ module @recursive { // ----- // Test parametric gates and wires + +// CHECK-LABEL: module @param_rxry module @param_rxry { func.func public @test_param_rxry(%arg0: tensor, %arg1: tensor) -> tensor<2xf64> attributes {quantum.node} { %c0_i64 = arith.constant 0 : i64 @@ -209,7 +217,7 @@ module @param_rxry { } // Decomposition function expects tensor while operation provides f64 - // CHECK-NOT: func.func private @ParametrizedRX_decomp + // CHECK: func.func private @ParametrizedRXRY_decomp func.func private @ParametrizedRXRY_decomp(%arg0: tensor, %arg1: !quantum.bit) -> !quantum.bit attributes {target_gate = "ParametrizedRXRY", llvm.linkage = #llvm.linkage} { %extracted = tensor.extract %arg0[] : tensor @@ -219,92 +227,60 @@ module @param_rxry { return %out_qubits_1 : !quantum.bit } } -// ----- - -// Test parametric gates and wires -module @param_rxry_2 { - func.func public @test_param_rxry_2(%arg0: tensor, %arg1: tensor, %arg2: tensor) -> tensor<2xf64> attributes {quantum.node} { - %c0_i64 = arith.constant 0 : i64 - - // CHECK: [[REG:%.+]] = quantum.alloc( 1) : !quantum.reg - %0 = quantum.alloc( 1) : !quantum.reg - - // CHECK: [[WIRE:%.+]] = tensor.extract %arg2[] : tensor - %extracted = tensor.extract %arg2[] : tensor - - // CHECK: [[QUBIT:%.+]] = quantum.extract [[REG]][[[WIRE]]] : !quantum.reg -> !quantum.bit - %1 = quantum.extract %0[%extracted] : !quantum.reg -> !quantum.bit - - // CHECK: [[PARAM_0:%.+]] = tensor.extract %arg0[] : tensor - %param_0 = tensor.extract %arg0[] : tensor - - // CHECK: [[PARAM_1:%.+]] = tensor.extract %arg1[] : tensor - %param_1 = tensor.extract %arg1[] : tensor - - // CHECK: [[QUBIT1:%.+]] = quantum.custom "RX"([[PARAM_0]]) [[QUBIT]] : !quantum.bit - // CHECK: [[QUBIT2:%.+]] = quantum.custom "RY"([[PARAM_1]]) [[QUBIT1]] : !quantum.bit - // CHECK-NOT: quantum.custom "ParametrizedRXRY" - %out_qubits = quantum.custom "ParametrizedRXRY"(%param_0, %param_1) %1 : !quantum.bit - - // CHECK: [[UPDATED_REG:%.+]] = quantum.insert [[REG]][ 0], [[QUBIT2]] : !quantum.reg, !quantum.bit - %2 = quantum.insert %0[ 0], %out_qubits : !quantum.reg, !quantum.bit - %3 = quantum.compbasis qreg %2 : !quantum.obs - %4 = quantum.probs %3 : tensor<2xf64> - quantum.dealloc %2 : !quantum.reg - return %4 : tensor<2xf64> - } - // Decomposition function expects tensor while operation provides f64 - // CHECK-NOT: func.func private @ParametrizedRX_decomp - func.func private @ParametrizedRXRY_decomp(%arg0: tensor, %arg1: tensor, %arg2: !quantum.bit) -> !quantum.bit - attributes {target_gate = "ParametrizedRXRY", llvm.linkage = #llvm.linkage} { - %extracted_param_0 = tensor.extract %arg0[] : tensor - %out_qubits = quantum.custom "RX"(%extracted_param_0) %arg2 : !quantum.bit - %extracted_param_1 = tensor.extract %arg1[] : tensor - %out_qubits_1 = quantum.custom "RY"(%extracted_param_1) %out_qubits : !quantum.bit - return %out_qubits_1 : !quantum.bit - } -} // ----- // Test recursive and qreg-based gate decomposition + +// CHECK-LABEL: module @qreg_base_circuit module @qreg_base_circuit { func.func public @test_qreg_base_circuit() -> tensor<2xf64> attributes {quantum.node} { - // CHECK: [[CST:%.+]] = arith.constant 1.000000e+00 : f64 + // CHECK-DAG: [[cmp_0:%.+]] = stablehlo.constant dense<0.000000e+00> : tensor + // CHECK-DAG: [[test_angle:%.+]] = arith.constant 1.000000e+00 : f64 + // CHECK-DAG: [[index_tensor:%.+]] = arith.constant dense<0> : tensor<1xi64> + // CHECK-DAG: [[cmp_1:%.+]] = arith.constant dense<1.000000e+00> : tensor + // CHECK: [[reg0:%.+]] = quantum.alloc( 1) : !quantum.reg %cst = arith.constant 1.000000e+00 : f64 - - // CHECK: [[CST_0:%.+]] = stablehlo.constant dense<0.000000e+00> : tensor - // CHECK: [[CST_1:%.+]] = arith.constant dense<0> : tensor<1xi64> - // CHECK: [[CST_2:%.+]] = arith.constant dense<1.000000e+00> : tensor - // CHECK: [[REG:%.+]] = quantum.alloc( 1) : !quantum.reg %0 = quantum.alloc( 1) : !quantum.reg - // CHECK: [[EXTRACT_QUBIT:%.+]] = quantum.extract [[REG]][ 0] : !quantum.reg -> !quantum.bit - // CHECK: [[MRES:%.+]], [[OUT_QUBIT:%.+]] = quantum.measure [[EXTRACT_QUBIT]] : i1, !quantum.bit - // CHECK: [[REG1:%.+]] = quantum.insert [[REG]][ 0], [[OUT_QUBIT]] : !quantum.reg, !quantum.bit - // CHECK: [[COMPARE:%.+]] = stablehlo.compare NE, [[CST_2]], [[CST_0]], FLOAT : (tensor, tensor) -> tensor - // CHECK: [[EXTRACTED:%.+]] = tensor.extract [[COMPARE]][] : tensor - // CHECK: [[CONDITIONAL:%.+]] = scf.if [[EXTRACTED]] -> (!quantum.reg) { - // CHECK: [[SLICE1:%.+]] = stablehlo.slice [[CST_1]] [0:1] : (tensor<1xi64>) -> tensor<1xi64> - // CHECK: [[RESHAPE1:%.+]] = stablehlo.reshape [[SLICE1]] : (tensor<1xi64>) -> tensor - // CHECK: [[EXTRACTED_3:%.+]] = tensor.extract [[RESHAPE1]][] : tensor - // CHECK: [[FROM_ELEMENTS:%.+]] = tensor.from_elements [[EXTRACTED_3]] : tensor<1xi64> - // CHECK: [[SLICE2:%.+]] = stablehlo.slice [[FROM_ELEMENTS]] [0:1] : (tensor<1xi64>) -> tensor<1xi64> - // CHECK: [[RESHAPE2:%.+]] = stablehlo.reshape [[SLICE2]] : (tensor<1xi64>) -> tensor - // CHECK: [[EXTRACTED_4:%.+]] = tensor.extract [[RESHAPE2]][] : tensor - // CHECK: [[EXTRACT1:%.+]] = quantum.extract [[REG1]][[[EXTRACTED_4]]] : !quantum.reg -> !quantum.bit - // CHECK: [[RZ1:%.+]] = quantum.custom "RZ"([[CST]]) [[EXTRACT1]] : !quantum.bit - // CHECK: [[INSERT1:%.+]] = quantum.insert [[REG1]][[[EXTRACTED_4]]], [[RZ1]] : !quantum.reg, !quantum.bit - // CHECK: [[EXTRACT2:%.+]] = quantum.extract [[INSERT1]][[[EXTRACTED_3]]] : !quantum.reg -> !quantum.bit - // CHECK: [[INSERT2:%.+]] = quantum.insert [[REG1]][[[EXTRACTED_3]]], [[EXTRACT2]] : !quantum.reg, !quantum.bit - // CHECK: [[EXTRACT3:%.+]] = quantum.extract [[INSERT2]][[[EXTRACTED_4]]] : !quantum.reg -> !quantum.bit - // CHECK: [[RZ2:%.+]] = quantum.custom "RZ"([[CST]]) [[EXTRACT3]] : !quantum.bit - // CHECK: [[INSERT3:%.+]] = quantum.insert [[INSERT2]][[[EXTRACTED_4]]], [[RZ2]] : !quantum.reg, !quantum.bit - // CHECK: [[EXTRACT4:%.+]] = quantum.extract [[INSERT3]][[[EXTRACTED_3]]] : !quantum.reg -> !quantum.bit - // CHECK: [[INSERT4:%.+]] = quantum.insert [[INSERT2]][[[EXTRACTED_3]]], [[EXTRACT4]] : !quantum.reg, !quantum.bit - // CHECK: scf.yield [[INSERT4]] : !quantum.reg + + // CHECK: [[q0:%.+]] = quantum.extract [[reg0]][ 0] : !quantum.reg -> !quantum.bit + // CHECK: [[meas:%.+]], [[q1:%.+]] = quantum.measure [[q0]] : i1, !quantum.bit + // CHECK: [[reg1:%.+]] = quantum.insert [[reg0]][ 0], [[q1]] : !quantum.reg, !quantum.bit + // CHECK: [[cmp:%.+]] = stablehlo.compare NE, [[cmp_1]], [[cmp_0]], FLOAT : (tensor, tensor) -> tensor + // CHECK: [[cond:%.+]] = tensor.extract [[cmp]][] : tensor + // CHECK: [[condresult:%.+]] = scf.if [[cond]] -> (!quantum.reg) { + // CHECK: [[slice0:%.+]] = stablehlo.slice [[index_tensor]] [0:1] : (tensor<1xi64>) -> tensor<1xi64> + // CHECK: [[reshape0:%.+]] = stablehlo.reshape [[slice0]] : (tensor<1xi64>) -> tensor + // CHECK: [[index0:%.+]] = tensor.extract [[reshape0]][] : tensor + // CHECK: [[fromelements0:%.+]] = tensor.from_elements [[index0]] : tensor<1xi64> + // CHECK: [[slice1:%.+]] = stablehlo.slice [[fromelements0]] [0:1] : (tensor<1xi64>) -> tensor<1xi64> + // CHECK: [[reshape1:%.+]] = stablehlo.reshape [[slice1]] : (tensor<1xi64>) -> tensor + // CHECK: [[index1:%.+]] = tensor.extract [[reshape1]][] : tensor + // CHECK: [[q2:%.+]] = quantum.extract [[reg1]][[[index1]]] : !quantum.reg -> !quantum.bit + // CHECK: [[q3:%.+]] = quantum.custom "RZ"([[test_angle]]) [[q2]] : !quantum.bit + // CHECK: [[index2:%.+]] = tensor.extract [[reshape1]][] + // CHECK: [[reg2:%.+]] = quantum.insert [[reg1]][[[index2]]], [[q3]] : !quantum.reg, !quantum.bit + // CHECK: [[q4:%.+]] = quantum.extract [[reg2]][[[index0]]] : !quantum.reg -> !quantum.bit + // CHECK: [[slice3:%.+]] = stablehlo.slice [[index_tensor]] [0:1] : (tensor<1xi64>) -> tensor<1xi64> + // CHECK: [[reshape3:%.+]] = stablehlo.reshape [[slice3]] : (tensor<1xi64>) -> tensor + // CHECK: [[extract6:%.+]] = tensor.extract [[reshape0]][] + // CHECK: [[reg3:%.+]] = quantum.insert [[reg1]][[[extract6]]], [[q4]] : !quantum.reg, !quantum.bit + // CHECK: [[index3:%.+]] = tensor.extract [[reshape3]][] + // CHECK: [[fromelements1:%.+]] = tensor.from_elements [[index3]] + // CHECK: [[slice4:%.+]] = stablehlo.slice [[fromelements1]] [0:1] + // CHECK: [[reshape4:%.+]] = stablehlo.reshape [[slice4]] + // CHECK: [[index4:%.+]] = tensor.extract [[reshape4]][] + // CHECK: [[q5:%.+]] = quantum.extract [[reg3]][[[index4]]] : !quantum.reg -> !quantum.bit + // CHECK: [[q6:%.+]] = quantum.custom "RZ"([[test_angle]]) [[q5]] : !quantum.bit + // CHECK: [[index5:%.+]] = tensor.extract [[reshape4]][] + // CHECK: [[reg4:%.+]] = quantum.insert [[reg3]][[[index5]]], [[q6]] : !quantum.reg, !quantum.bit + // CHECK: [[q7:%.+]] = quantum.extract [[reg4]][[[index3]]] : !quantum.reg -> !quantum.bit + // CHECK: [[index6:%.+]] = tensor.extract [[reshape3]][] + // CHECK: [[out:%.+]] = quantum.insert [[reg3]][[[index6]]], [[q7]] : !quantum.reg, !quantum.bit + // CHECK: scf.yield [[out]] : !quantum.reg // CHECK: } else { - // CHECK: scf.yield [[REG1]] : !quantum.reg + // CHECK: scf.yield [[reg1]] : !quantum.reg // CHECK: } // CHECK-NOT: quantum.custom "Test" %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit @@ -318,8 +294,8 @@ module @qreg_base_circuit { return %4 : tensor<2xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @Test_rule_1 + // Decomposition function should be retained for future passes + // CHECK: func.func private @Test_rule_1 func.func private @Test_rule_1(%arg0: !quantum.reg, %arg1: tensor, %arg2: tensor<1xi64>) -> !quantum.reg attributes {target_gate = "Test", llvm.linkage = #llvm.linkage} { %cst = stablehlo.constant dense<0.000000e+00> : tensor @@ -352,8 +328,8 @@ module @qreg_base_circuit { return %1 : !quantum.reg } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @RzDecomp_rule_1 + // Decomposition function should be retained for future passes + // CHECK: func.func private @RzDecomp_rule_1 func.func private @RzDecomp_rule_1(%arg0: !quantum.reg, %arg1: tensor, %arg2: tensor<1xi64>) -> !quantum.reg attributes {target_gate = "RzDecomp", llvm.linkage = #llvm.linkage} { %0 = stablehlo.slice %arg2 [0:1] : (tensor<1xi64>) -> tensor<1xi64> @@ -370,12 +346,12 @@ module @qreg_base_circuit { // ----- +// CHECK-LABEL: module @multi_wire_cnot_decomposition module @multi_wire_cnot_decomposition { func.func public @test_cnot_decomposition() -> tensor<4xf64> attributes {quantum.node} { %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit - // CHECK: [[CST_PI:%.+]] = arith.constant 3.1415926535897931 : f64 // CHECK: [[CST_PI2:%.+]] = arith.constant 1.5707963267948966 : f64 // CHECK: [[WIRE_TENSOR:%.+]] = arith.constant dense<[0, 1]> : tensor<2xi64> @@ -388,13 +364,22 @@ module @multi_wire_cnot_decomposition { // CHECK: [[QUBIT1:%.+]] = quantum.extract [[REG]][[[EXTRACTED]]] : !quantum.reg -> !quantum.bit // CHECK: [[RZ1:%.+]] = quantum.custom "RZ"([[CST_PI]]) [[QUBIT1]] : !quantum.bit // CHECK: [[RY1:%.+]] = quantum.custom "RY"([[CST_PI2]]) [[RZ1]] : !quantum.bit + // CHECK: [[index:%.+]] = tensor.extract [[RESHAPE2]][] + // CHECK: [[INSERT_TARGET:%.+]] = quantum.insert [[REG]][[[index]]], [[RY1]] : !quantum.reg, !quantum.bit // CHECK: [[EXTRACTED2:%.+]] = tensor.extract [[RESHAPE1]][] : tensor - // CHECK: [[QUBIT0:%.+]] = quantum.extract [[REG]][[[EXTRACTED2]]] : !quantum.reg -> !quantum.bit - // CHECK: [[CZ_RESULT:%.+]]:2 = quantum.custom "CZ"() [[QUBIT0]], [[RY1]] : !quantum.bit, !quantum.bit - // CHECK: [[INSERT2:%.+]] = quantum.insert [[REG]][[[EXTRACTED2]]], [[CZ_RESULT]]#0 : !quantum.reg, !quantum.bit - // CHECK: [[RZ2:%.+]] = quantum.custom "RZ"([[CST_PI]]) [[CZ_RESULT]]#1 : !quantum.bit + // CHECK: [[QUBIT0:%.+]] = quantum.extract [[INSERT_TARGET]][[[EXTRACTED2]]] : !quantum.reg -> !quantum.bit + // CHECK: [[index2:%.+]] = tensor.extract [[RESHAPE2]][] + // CHECK: [[QUBIT1_UPDATED:%.+]] = quantum.extract [[INSERT_TARGET]][[[index2]]] : !quantum.reg -> !quantum.bit + // CHECK: [[CZ_RESULT:%.+]]:2 = quantum.custom "CZ"() [[QUBIT0]], [[QUBIT1_UPDATED]] : !quantum.bit, !quantum.bit + // CHECK: [[index3:%.+]] = tensor.extract [[RESHAPE1]][] + // CHECK: [[INSERT2:%.+]] = quantum.insert [[INSERT_TARGET]][[[index3]]], [[CZ_RESULT]]#0 : !quantum.reg, !quantum.bit + // CHECK: [[index4:%.+]] = tensor.extract [[RESHAPE2]][] + // CHECK: [[INSERT_CZ1:%.+]] = quantum.insert [[INSERT2]][[[index4]]], [[CZ_RESULT]]#1 : !quantum.reg, !quantum.bit + // CHECK: [[TARGET_AFTER_CZ:%.+]] = quantum.extract [[INSERT_CZ1]][{{%.+}}] : !quantum.reg -> !quantum.bit + // CHECK: [[RZ2:%.+]] = quantum.custom "RZ"([[CST_PI]]) [[TARGET_AFTER_CZ]] : !quantum.bit // CHECK: [[RY2:%.+]] = quantum.custom "RY"([[CST_PI2]]) [[RZ2]] : !quantum.bit - // CHECK: [[INSERT3:%.+]] = quantum.insert [[INSERT2]][[[EXTRACTED]]], [[RY2]] : !quantum.reg, !quantum.bit + // CHECK: [[index5:%.+]] = tensor.extract [[RESHAPE2]][] + // CHECK: [[INSERT3:%.+]] = quantum.insert [[INSERT_CZ1]][[[index5]]], [[RY2]] : !quantum.reg, !quantum.bit // CHECK: [[FINAL_QUBIT0:%.+]] = quantum.extract [[INSERT3]][ 0] : !quantum.reg -> !quantum.bit // CHECK: [[FINAL_QUBIT1:%.+]] = quantum.extract [[INSERT3]][ 1] : !quantum.reg -> !quantum.bit // CHECK-NOT: quantum.custom "CNOT" @@ -410,8 +395,8 @@ module @multi_wire_cnot_decomposition { return %8 : tensor<4xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @CNOT_rule_cz_rz_ry + // Decomposition function should be retained for future passes + // CHECK: func.func private @CNOT_rule_cz_rz_ry func.func private @CNOT_rule_cz_rz_ry(%arg0: !quantum.reg, %arg1: tensor<2xi64>) -> !quantum.reg attributes {target_gate = "CNOT", llvm.linkage = #llvm.linkage} { // CNOT decomposition: CNOT = (I ⊗ H) * CZ * (I ⊗ H) %cst = arith.constant 1.5707963267948966 : f64 @@ -456,6 +441,7 @@ module @multi_wire_cnot_decomposition { // ----- +// CHECK-LABEL: module @cnot_alternative_decomposition module @cnot_alternative_decomposition { func.func public @test_cnot_alternative_decomposition() -> tensor<4xf64> attributes {quantum.node} { %0 = quantum.alloc( 2) : !quantum.reg @@ -485,8 +471,8 @@ module @cnot_alternative_decomposition { return %8 : tensor<4xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func private @CNOT_rule_h_cnot_h + // Decomposition function should be retained for future passes + // CHECK: func.func private @CNOT_rule_h_cnot_h func.func private @CNOT_rule_h_cnot_h(%arg0: !quantum.bit, %arg1: !quantum.bit) -> (!quantum.bit, !quantum.bit) attributes {target_gate = "CNOT", llvm.linkage = #llvm.linkage} { // CNOT decomposition: CNOT = (I ⊗ H) * CZ * (I ⊗ H) %cst = arith.constant 1.5707963267948966 : f64 @@ -509,6 +495,7 @@ module @cnot_alternative_decomposition { // ----- +// CHECK-LABEL: module @mcm_example module @mcm_example { func.func public @test_mcm_hadamard() -> tensor<2xf64> attributes {quantum.node} { %0 = quantum.alloc( 1) : !quantum.reg @@ -516,9 +503,9 @@ module @mcm_example { %mres, %out_qubit = quantum.measure %1 : i1, !quantum.bit %2 = quantum.insert %0[ 0], %out_qubit : !quantum.reg, !quantum.bit - // CHECK: [[RZ_QUBIT:%.+]] = quantum.custom "RZ"([[CST_0:%.+]]) - // CHECK: [[RY_QUBIT:%.+]] = quantum.custom "RY"([[CST_1:%.+]]) [[RZ_QUBIT]] : !quantum.bit - // CHECK: [[REG_1:%.+]] = quantum.insert [[REG:%.+]][[[EXTRACTED:%.+]]], [[RY_QUBIT]] : !quantum.reg, !quantum.bit + // CHECK: quantum.custom "RZ" + // CHECK: quantum.custom "RY" + // CHECK-NOT: quantum.custom "Hadamard" %3 = quantum.extract %2[ 0] : !quantum.reg -> !quantum.bit %out_qubits = quantum.custom "Hadamard"() %3 : !quantum.bit @@ -530,8 +517,8 @@ module @mcm_example { return %6 : tensor<2xf64> } - // Decomposition function should be applied and removed from the module - // CHECK-NOT: func.func public @rz_ry + // Decomposition function should be retained for future passes + // CHECK: func.func public @rz_ry func.func public @rz_ry(%arg0: !quantum.reg, %arg1: tensor<1xi64>) -> !quantum.reg attributes {llvm.linkage = #llvm.linkage, num_wires = 1 : i64, target_gate = "Hadamard"} { %cst = arith.constant 3.1415926535897931 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 @@ -555,15 +542,16 @@ module @mcm_example { // ----- +// CHECK-LABEL: module @circuit_with_multirz module @circuit_with_multirz { func.func public @test_with_multirz() -> tensor<4xf64> attributes {quantum.node} { %0 = quantum.alloc( 2) : !quantum.reg %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit // CHECK: func.func public @test_with_multirz() -> tensor<4xf64> - // CHECK: [[CST_RZ:%.+]] = arith.constant 5.000000e-01 : f64 - // CHECK: [[CST_PI2:%.+]] = arith.constant 1.5707963267948966 : f64 - // CHECK: [[CST_PI:%.+]] = arith.constant 3.1415926535897931 : f64 - // CHECK: [[REG:%.+]] = quantum.alloc( 2) : !quantum.reg + // CHECK-DAG: [[CST_PI2:%.+]] = arith.constant 1.5707963267948966 : f64 + // CHECK-DAG: [[CST_PI:%.+]] = arith.constant 3.1415926535897931 : f64 + // CHECK-DAG: [[CST_RZ:%.+]] = arith.constant 5.000000e-01 : f64 + // CHECK-DAG: [[REG:%.+]] = quantum.alloc( 2) : !quantum.reg // CHECK: [[QUBIT1:%.+]] = quantum.custom "RZ"([[CST_RZ]]) {{%.+}} : !quantum.bit // CHECK-NOT: quantum.multirz @@ -584,7 +572,7 @@ module @circuit_with_multirz { return %4 : tensor<4xf64> } - // CHECK-NOT: func.func private @Hadamard_to_RY_decomp + // CHECK: func.func private @Hadamard_to_RY_decomp func.func private @Hadamard_to_RY_decomp(%arg0: !quantum.bit) -> !quantum.bit attributes {target_gate = "Hadamard", llvm.linkage = #llvm.linkage} { %cst = arith.constant 3.1415926535897931 : f64 %cst_0 = arith.constant 1.5707963267948966 : f64 @@ -593,8 +581,8 @@ module @circuit_with_multirz { return %out_qubits_1 : !quantum.bit } - // CHECK-NOT: func.func private @_multi_rz_decomposition_wires_1 - func.func public @_multi_rz_decomposition_wires_1(%arg0: !quantum.reg, %arg1: tensor<1xf64>, %arg2: tensor<1xi64>) -> !quantum.reg attributes {llvm.linkage = #llvm.linkage, num_wires = 1 : i64, target_gate = "MultiRZ"} { + // CHECK: func.func private @_multi_rz_decomposition_wires_1 + func.func private @_multi_rz_decomposition_wires_1(%arg0: !quantum.reg, %arg1: tensor<1xf64>, %arg2: tensor<1xi64>) -> !quantum.reg attributes {llvm.linkage = #llvm.linkage, num_wires = 1 : i64, target_gate = "MultiRZ"} { %0 = stablehlo.slice %arg2 [0:1] : (tensor<1xi64>) -> tensor<1xi64> %1 = stablehlo.reshape %0 : (tensor<1xi64>) -> tensor %extracted = tensor.extract %1[] : tensor @@ -610,6 +598,7 @@ module @circuit_with_multirz { // ----- +// CHECK-LABEL: module @qreg_at_not_first_arg module @qreg_at_not_first_arg { func.func public @test_qreg_at_not_first_arg() attributes {quantum.node} { // CHECK: [[wire_tensor:%.+]] = arith.constant dense<[0, 1]> : tensor<2xi64> @@ -635,7 +624,7 @@ module @qreg_at_not_first_arg { return } - // CHECK-NOT: func.func private @my_cnot + // CHECK: func.func private @my_cnot func.func private @my_cnot(%arg0: tensor<2xi64>, %arg1: !quantum.reg) -> !quantum.reg attributes {target_gate = "CNOT"} { %0 = stablehlo.slice %arg0 [0:1] : (tensor<2xi64>) -> tensor<1xi64> %1 = stablehlo.reshape %0 : (tensor<1xi64>) -> tensor @@ -654,6 +643,7 @@ module @qreg_at_not_first_arg { // ----- +// CHECK-LABEL: module @test_paulirot module @test_paulirot { func.func @test() attributes {quantum.node} { %pi = arith.constant 3.1 : f64 @@ -678,7 +668,7 @@ module @test_paulirot { return } - // CHECK-NOT: my_paulirot_decomp + // CHECK: my_paulirot_decomp func.func private @my_paulirot_decomp(%inreg : !quantum.reg, %angle_tensor : tensor, %q_tensor : tensor<3xi64>) -> !quantum.reg attributes {target_gate = "paulirotZXY"} { %pi_by_2 = arith.constant 1.57 : f64 %m_pi_by_2 = arith.constant -1.57 : f64 @@ -717,6 +707,7 @@ module @test_paulirot { // ----- +// CHECK-LABEL: module @null_decomp_rule module @null_decomp_rule{ func.func public @test_null_decomp_rule() attributes {quantum.node} { // CHECK: [[reg:%.+]] = quantum.alloc( 1) @@ -733,7 +724,7 @@ module @null_decomp_rule{ return } - // CHECK-NOT: func.func private @null_decomp + // CHECK: func.func private @null_decomp func.func private @null_decomp() attributes {target_gate = "PauliX"} { return } @@ -741,6 +732,7 @@ module @null_decomp_rule{ // ----- +// CHECK-LABEL: module @different_qreg_values module @different_qreg_values{ func.func public @circuit() attributes {quantum.node} { // CHECK: [[wire_tensor:%.+]] = arith.constant dense<[2, 1]> : tensor<2xi64> @@ -776,7 +768,7 @@ module @different_qreg_values{ return } - // CHECK-NOT: func.func private @my_cnot + // CHECK: func.func private @my_cnot func.func private @my_cnot(%arg0: tensor<2xi64>, %arg1: !quantum.reg) -> !quantum.reg attributes {target_gate = "CNOT"} { %0 = stablehlo.slice %arg0 [0:1] : (tensor<2xi64>) -> tensor<1xi64> %1 = stablehlo.reshape %0 : (tensor<1xi64>) -> tensor From 0391ff5aff9bcb50ffff66badd73df81aa89a892 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 24 Jun 2026 16:01:42 -0400 Subject: [PATCH 07/78] update graph-decomposition --- mlir/lib/Quantum/Transforms/graph_decomposition.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index a8bf13b12f..df057502c9 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -136,8 +136,11 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasepush_back(rule.release()); + if (!symbolTable.lookup(rule->getName())) { + module.getBody()->push_back(rule.release()); + } } } From 689ada3a64da5bf47ad150ed74c4a8f51fe72dee Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Thu, 25 Jun 2026 09:42:58 -0400 Subject: [PATCH 08/78] cleanup --- .../Transforms/DecomposeLoweringPatterns.cpp | 54 ++++++++++--------- .../Quantum/Transforms/decompose_lowering.cpp | 4 +- .../Transforms/graph_decomposition.cpp | 28 ++++++++-- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp b/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp index 4e80e49c43..5f7c88878c 100644 --- a/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp +++ b/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp @@ -40,16 +40,16 @@ #include "DecomposeLoweringImpl.hpp" +#define DEBUG_TYPE "decompose-lowering" + using namespace mlir; using namespace catalyst::quantum; -#define DEBUG_TYPE "decompose-lowering" - namespace catalyst { namespace quantum { -SmallVector inlineDecompositionRule(func::FuncOp rule, ValueRange operands, - PatternRewriter &rewriter) +SmallVector getDecompRuleResults(func::FuncOp rule, ValueRange operands, + PatternRewriter &rewriter) { Block &body = rule.front(); auto returnOp = cast(body.getTerminator()); @@ -103,6 +103,8 @@ struct DLCustomOpPattern : public OpRewritePattern { return failure(); } + // do not nest decomposition rules, they're applied greedily and this can lead to + // cycles/identity rules if (isInDecompRule(op)) { return failure(); } @@ -144,17 +146,17 @@ struct DLCustomOpPattern : public OpRewritePattern { assert(analyzer && "Analyzer should be valid"); auto operands = analyzer.prepareOperands(rule, rewriter, op.getLoc()); - SmallVector inlinedFunction = inlineDecompositionRule(rule, operands, rewriter); + SmallVector inlinedFunctionResults = getDecompRuleResults(rule, operands, rewriter); // Replace the op with the inlined function and adjust the insert ops for the qreg mode - if (inlinedFunction.size() == 1 && - isa(inlinedFunction.front().getType())) { - auto results = - analyzer.prepareResultsForQreg(inlinedFunction.front(), op.getLoc(), rewriter); + if (inlinedFunctionResults.size() == 1 && + isa(inlinedFunctionResults.front().getType())) { + auto results = analyzer.prepareResultsForQreg(inlinedFunctionResults.front(), + op.getLoc(), rewriter); rewriter.replaceOp(op, results); } else { - rewriter.replaceOp(op, inlinedFunction); + rewriter.replaceOp(op, inlinedFunctionResults); } return success(); @@ -183,6 +185,8 @@ struct DLMultiRZOpPattern : public OpRewritePattern { return failure(); } + // do not nest decomposition rules, they're applied greedily and this can lead to + // cycles/identity rules if (isInDecompRule(op)) { return failure(); } @@ -223,18 +227,18 @@ struct DLMultiRZOpPattern : public OpRewritePattern { assert(analyzer && "Analyzer should be valid"); auto operands = analyzer.prepareOperands(decompFunc, rewriter, op.getLoc()); - SmallVector inlinedFunction = - inlineDecompositionRule(decompFunc, operands, rewriter); + SmallVector inlinedFunctionResults = + getDecompRuleResults(decompFunc, operands, rewriter); // Replace the op with the inlined function and adjust the insert ops for the qreg mode - if (inlinedFunction.size() == 1 && - isa(inlinedFunction.front().getType())) { - auto results = - analyzer.prepareResultsForQreg(inlinedFunction.front(), op.getLoc(), rewriter); + if (inlinedFunctionResults.size() == 1 && + isa(inlinedFunctionResults.front().getType())) { + auto results = analyzer.prepareResultsForQreg(inlinedFunctionResults.front(), + op.getLoc(), rewriter); rewriter.replaceOp(op, results); } else { - rewriter.replaceOp(op, inlinedFunction); + rewriter.replaceOp(op, inlinedFunctionResults); } return success(); @@ -263,6 +267,8 @@ struct DLPauliRotOpPattern : public OpRewritePattern { return failure(); } + // do not nest decomposition rules, they're applied greedily and this can lead to + // cycles/identity rules if (isInDecompRule(op)) { return failure(); } @@ -289,18 +295,18 @@ struct DLPauliRotOpPattern : public OpRewritePattern { assert(analyzer && "Analyzer should be valid"); auto operands = analyzer.prepareOperands(decompFunc, rewriter, op.getLoc()); - SmallVector inlinedFunction = - inlineDecompositionRule(decompFunc, operands, rewriter); + SmallVector inlinedFunctionResults = + getDecompRuleResults(decompFunc, operands, rewriter); // Replace the op with the inlined results and adjust the insert ops for the qreg mode - if (inlinedFunction.size() == 1 && - isa(inlinedFunction.front().getType())) { - auto results = - analyzer.prepareResultsForQreg(inlinedFunction.front(), op.getLoc(), rewriter); + if (inlinedFunctionResults.size() == 1 && + isa(inlinedFunctionResults.front().getType())) { + auto results = analyzer.prepareResultsForQreg(inlinedFunctionResults.front(), + op.getLoc(), rewriter); rewriter.replaceOp(op, results); } else { - rewriter.replaceOp(op, inlinedFunction); + rewriter.replaceOp(op, inlinedFunctionResults); } return success(); diff --git a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp index 3b7671ac71..b1be26406c 100644 --- a/mlir/lib/Quantum/Transforms/decompose_lowering.cpp +++ b/mlir/lib/Quantum/Transforms/decompose_lowering.cpp @@ -45,11 +45,11 @@ #include "Quantum/IR/QuantumOps.h" #include "Quantum/Transforms/Patterns.h" +#define DEBUG_TYPE "decompose-lowering" + using namespace mlir; using namespace catalyst::quantum; -#define DEBUG_TYPE "decompose-lowering" - namespace catalyst { namespace quantum { diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index df057502c9..5a9425e9cb 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -12,27 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -#define DEBUG_TYPE "graph-decomposition" - +#include +#include +#include +#include +#include +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" +#include "llvm/Support/Casting.h" #include "llvm/Support/Debug.h" #include "llvm/Support/DebugLog.h" +#include "llvm/Support/raw_ostream.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/LLVMIR/LLVMDialect.h" #include "mlir/Dialect/MemRef/IR/MemRef.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/AsmState.h" +#include "mlir/IR/BuiltinAttributes.h" #include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/Location.h" #include "mlir/IR/OwningOpRef.h" +#include "mlir/IR/SymbolTable.h" #include "mlir/Parser/Parser.h" #include "mlir/Pass/PassManager.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Support/WalkResult.h" #include "stablehlo/dialect/StablehloOps.h" #include "Catalyst/Analysis/ResourceAnalysis.h" +#include "Catalyst/Analysis/ResourceResult.h" #include "Catalyst/Transforms/Passes.h" #include "QRef/Transforms/Passes.h" #include "Quantum/IR/QuantumDialect.h" +#include "Quantum/IR/QuantumInterfaces.h" #include "Quantum/IR/QuantumOps.h" #include "Quantum/Transforms/Passes.h" #include "Quantum/Transforms/QPDLoader.h" @@ -41,6 +61,8 @@ #include "DGSolver.hpp" #include "DGTypes.hpp" +#define DEBUG_TYPE "graph-decomposition" + using namespace mlir; using namespace catalyst::quantum; using namespace DecompGraph::Core; @@ -135,7 +157,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(rule->getName())) { From 3648480198859f05155d6e49a97db1c3939ea90c Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Thu, 25 Jun 2026 13:12:49 -0400 Subject: [PATCH 09/78] update pass --- .../Transforms/graph_decomposition.cpp | 237 +++++++++--------- 1 file changed, 122 insertions(+), 115 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index 5a9425e9cb..351c492537 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -128,7 +128,9 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(rule->getName())) { - module.getBody()->push_back(rule.release()); - } - } } private: @@ -236,36 +231,79 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase> &ruleRegistry) + LogicalResult addRuleNode(mlir::func::FuncOp rule, std::vector &ruleNodes) + { + llvm::StringRef ruleName = rule.getName(); + + // 1. Mandatory Attribute Check (Target Gate and Resources) + auto targetGateAttr = rule->getAttrOfType("target_gate"); + auto resourcesAttr = rule->getAttrOfType("resources"); + if (!targetGateAttr || !resourcesAttr) { + llvm::errs() << "Cannot parse decomposition rule without `target_gate` and `resources` " + "attributes.\n"; + return failure(); + } + + // 2. Extract 'operations' dictionary from resources + auto operations = mlir::dyn_cast_or_null(resourcesAttr.get("operations")); + if (!operations) { + llvm::errs() + << "Cannot parse decomposition rule resources without `operations` attribute.\n"; + return failure(); + } + + // 3. Populate RuleNode + RuleNode ruleNode; + ruleNode.name = ruleName.str(); + ruleNode.output = parseOperator(targetGateAttr.getValue()); + + for (const auto &namedAttr : operations) { + if (auto intAttr = mlir::dyn_cast(namedAttr.getValue())) { + ruleNode.inputs.push_back({parseOperator(namedAttr.getName().strref()), + static_cast(intAttr.getInt())}); + } + } + + // 4. Add RuleNode + ruleNodes.push_back(std::move(ruleNode)); + return success(); + } + + LogicalResult loadBuiltInDecompositionRules(llvm::StringRef filename, + std::vector &ruleNodes) { mlir::MLIRContext *context = &getContext(); + mlir::ModuleOp module = getOperation(); mlir::ParserConfig config(context); mlir::OwningOpRef moduleOp = mlir::parseSourceFile(filename, config); + SymbolTable symbolTable(module); + if (!moduleOp) { - mlir::emitError(mlir::UnknownLoc::get(context)) - << "failed to load built-in decomposition rules from '" << filename - << "': the rules file could not be parsed"; - return; + llvm::errs() << "failed to load built-in decomposition rules from '" << filename + << "': the rules file could not be parsed\n"; + return failure(); } for (auto rule : llvm::make_early_inc_range(moduleOp.get().getOps())) { - rule->remove(); - ruleRegistry.push_back(std::move(rule)); + if (failed(addRuleNode(rule, ruleNodes))) { + return failure(); + } + // avoid double-insertion + if (!symbolTable.lookup(rule.getName())) { + rule->remove(); + module.push_back(std::move(rule)); + } } - return; + return success(); } /** - * @brief Remove user rules from the module, loading into + * @brief Load the listed user rules into the registry. */ - LogicalResult - loadUserDecompositionRules(llvm::StringSet<> &userRuleNames, - llvm::SmallVector> &graphRules, - llvm::SmallVector> &rules) + LogicalResult loadUserDecompositionRules(llvm::StringSet<> &userRuleNames, + std::vector &ruleNodes) { mlir::ModuleOp module = getOperation(); if (userRuleNames.empty()) { @@ -280,22 +318,21 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase userRules; - - module.walk([&](mlir::func::FuncOp func) { + WalkResult walkResult = module.walk([&](mlir::func::FuncOp func) { if (func->hasAttr("target_gate")) { - userRules.push_back(func); if (userRuleNames.contains(func.getName())) { - graphRules.push_back(mlir::OwningOpRef(func.clone())); + if (failed(addRuleNode(func, ruleNodes))) { + return WalkResult::interrupt(); + } } } return WalkResult::skip(); }); - for (auto rule : llvm::make_early_inc_range(userRules)) { - rule->remove(); - rules.push_back(std::move(rule)); + if (walkResult.wasInterrupted()) { + return failure(); } + return success(); } @@ -305,9 +342,9 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase> &ruleRegistry) + mlir::LogicalResult loadPauliRotRules(std::vector &ruleNodes) { + LDBG() << "loading paulirot rules"; mlir::ModuleOp module = getOperation(); MLIRContext *context = &getContext(); @@ -316,10 +353,20 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase pauliRotOps; module.walk([&](quantum::PauliRotOp op) { pauliRotOps.push_back(op); }); + LDBG() << "found the following paulirots:"; + for (auto op : pauliRotOps) { + LDBG() << op; + } + if (!pauliRotOps.empty()) { - loadQPD(libQPDPath, libpythonPath); + if (!loadQPD(libQPDPath, libpythonPath)) { + llvm::errs() << "failed to load libQuantumPythonCallbacks\n"; + return failure(); + } } + LDBG() << "loaded QPD"; + for (quantum::PauliRotOp pauliRot : pauliRotOps) { std::string pauliWord = pauliRot.getPauliWord(); @@ -365,15 +412,34 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasesetAttr("resources", buildResourceDict(context, *flat)); } - ruleRegistry.push_back(std::move(outOp)); + if (failed(addRuleNode(funcOp, ruleNodes))) { + return failure(); + } + LDBG() << "adding rule " << funcOp.getName(); + module.push_back(std::move(outOp.release())); } - return success(); } + bool isInDecompRule(Operation *op) + { + while (auto parentOp = op->getParentOp()) { + if (auto funcOp = dyn_cast(parentOp)) { + if (funcOp->hasAttr("target_gate")) { + return true; + } + } + op = parentOp; + } + return false; + } + void getOperators(std::vector &operators) { getOperation().walk([&](quantum::QuantumGate op) { + if (isInDecompRule(op)) { + return; + } OperatorNode node; node.numWires = op.getNonCtrlQubitOperands().size(); node.adjoint = op.getAdjointFlag(); @@ -455,84 +521,25 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &rules, - llvm::StringSet<> &userRuleNames, - llvm::SmallVector> &userRules, - llvm::StringMap> &ruleNameToFuncOp) + LogicalResult getRuleNodes(llvm::StringRef filename, std::vector &rules, + llvm::StringSet<> &userRuleNames) { - llvm::SmallVector> graphRules; - - // Load rules from bytecode and user-defined rules - loadBuiltInDecompositionRules(filename, graphRules); - if (failed(loadPauliRotRules(graphRules))) { - return signalPassFailure(); - } - if (failed(loadUserDecompositionRules(userRuleNames, graphRules, userRules))) { - return signalPassFailure(); - } - - for (auto &ruleOpRef : graphRules) { - mlir::func::FuncOp func = ruleOpRef.get(); - llvm::StringRef ruleName = func.getName(); - - // 1. Mandatory Attribute Check (Target Gate and Resources) - auto targetGateAttr = func->getAttrOfType("target_gate"); - auto resourcesAttr = func->getAttrOfType("resources"); - if (!targetGateAttr || !resourcesAttr) - continue; - - // 2. Extract 'operations' dictionary from resources - auto operations = - mlir::dyn_cast_or_null(resourcesAttr.get("operations")); - if (!operations) - continue; + LDBG() << "getting rule nodes"; - // 3. Populate RuleNode - RuleNode ruleNode; - ruleNode.name = ruleName.str(); - ruleNode.output = parseOperator(targetGateAttr.getValue()); - - for (const auto &namedAttr : operations) { - if (auto intAttr = mlir::dyn_cast(namedAttr.getValue())) { - ruleNode.inputs.push_back({parseOperator(namedAttr.getName().strref()), - static_cast(intAttr.getInt())}); - } - } + // Load pre-compiled rules (ignore failure, we can try to solve without) + std::ignore = loadBuiltInDecompositionRules(filename, rules); - // 4. Finalize: move the OpRef to the map to keep IR alive and store the node - ruleNameToFuncOp[ruleNode.name] = std::move(ruleOpRef); - rules.push_back(std::move(ruleNode)); + // Lower and load compile-time rules + LDBG() << "loading paulirot rules"; + if (failed(loadPauliRotRules(rules))) { + return failure(); } - } - /** - * @brief Insert the decomposition rules picked by the graph solver into the module for - * later use in the decompose-lowering patterns to apply the decomposition rules and rewrite - * the quantum operations. - * - * @param solution The chosen decomposition rules from the graph solver. - * @param ruleNameToFuncOp A mapping from rule names to their corresponding function - * operations. - */ - void insertChosenRules(GraphResult &solution, - llvm::StringMap> &ruleNameToFuncOp) - { - mlir::ModuleOp module = getOperation(); - for (const auto &[_, chosenRule] : solution) { - if (chosenRule.isBasis) { - continue; // skip basis rules as they don't correspond to actual decomposition - // functions to insert - } - auto it = ruleNameToFuncOp.find(chosenRule.ruleName); - - if (it == ruleNameToFuncOp.end() || !it->second) { - // skip if the rule is not found or - // the function op is null or - // it is already moved - continue; - } - module.push_back(it->second.release()); + // Load user-rules + if (failed(loadUserDecompositionRules(userRuleNames, rules))) { + return failure(); } + return success(); } /** From 9060f2249cd133c66059332f8855c3f5f5dc7b80 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Thu, 25 Jun 2026 13:12:56 -0400 Subject: [PATCH 10/78] update test --- mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir b/mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir index 4ade35c784..8c2d5d2e62 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir +++ b/mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir @@ -27,9 +27,13 @@ func.func @circuit() -> !quantum.bit { // XZ: PauliX // XZ: PauliZ %qout = quantum.custom "PauliY"() %q : !quantum.bit + + // needed to ensure we don't match in the following decomposition rules + // CHECK: return return %qout : !quantum.bit } +// CHECK-LABEL: y_to_ry func.func @y_to_ry(%q0 : !quantum.bit) -> !quantum.bit attributes {target_gate="PauliY"} { %pi = arith.constant 3.14 : f64 %negpiby2 = arith.constant -1.57 : f64 @@ -38,6 +42,7 @@ func.func @y_to_ry(%q0 : !quantum.bit) -> !quantum.bit attributes {target_gate=" return %q1 : !quantum.bit } +// CHECK-LABEL: y_to_x_z func.func @y_to_x_z(%q0 : !quantum.bit) -> !quantum.bit attributes {target_gate="PauliY"} { %q1 = quantum.custom "PauliX"() %q0 : !quantum.bit %q2 = quantum.custom "PauliZ"() %q1 : !quantum.bit From 8e55f6afe344615b01ebd559b3fbaa6b07492df5 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Thu, 25 Jun 2026 14:42:27 -0400 Subject: [PATCH 11/78] fix paulirot rule collisions, centralize resources in addRuleNode --- .../Transforms/graph_decomposition.cpp | 61 ++++++++----------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index 351c492537..c1ec170efb 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -104,10 +104,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase setOfOps; std::vector setOfRules; - llvm::StringMap> ruleNameToFuncOp; llvm::StringSet<> userRuleNames; - llvm::SmallVector> - allUserRules; // includes rules unused in this decomp llvm::StringMap opToFixedDecompName; llvm::StringMap> opToAltDecompNames; WeightedGateset targetGateSet; @@ -238,17 +235,28 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasegetAttrOfType("target_gate"); auto resourcesAttr = rule->getAttrOfType("resources"); - if (!targetGateAttr || !resourcesAttr) { - llvm::errs() << "Cannot parse decomposition rule without `target_gate` and `resources` " - "attributes.\n"; + if (!targetGateAttr) { + llvm::errs() << "Cannot parse decomposition rule " << ruleName + << " without the `target_gate` attribute.\n"; + LDBG() << rule; return failure(); } + // Ensure resources + if (!resourcesAttr) { + ResourceAnalysis analysis(rule); + if (const ResourceResult *flat = analysis.getFlattenedResource(rule.getName())) { + rule->setAttr("resources", buildResourceDict(&getContext(), *flat)); + } + resourcesAttr = rule->getAttrOfType("resources"); + } + // 2. Extract 'operations' dictionary from resources auto operations = mlir::dyn_cast_or_null(resourcesAttr.get("operations")); if (!operations) { - llvm::errs() - << "Cannot parse decomposition rule resources without `operations` attribute.\n"; + llvm::errs() << "Cannot parse resource for decomposition rule " << ruleName + << " without `operations` attribute.\n"; + LDBG() << rule; return failure(); } @@ -300,7 +308,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &userRuleNames, std::vector &ruleNodes) @@ -310,14 +318,6 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasehasAttr("target_gate")) { if (userRuleNames.contains(func.getName())) { @@ -344,20 +344,22 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &ruleNodes) { - LDBG() << "loading paulirot rules"; mlir::ModuleOp module = getOperation(); MLIRContext *context = &getContext(); llvm::StringSet<> addedWords; + // Add words from existing paulirot rules + module.walk([&](mlir::func::FuncOp func) { + if (func->hasAttr("target_gate")) { + if (func.getName().starts_with("paulirot_decomp_rule_")) { + addedWords.insert(func.getName().drop_front(21)); + } + } + }); llvm::SmallVector pauliRotOps; module.walk([&](quantum::PauliRotOp op) { pauliRotOps.push_back(op); }); - LDBG() << "found the following paulirots:"; - for (auto op : pauliRotOps) { - LDBG() << op; - } - if (!pauliRotOps.empty()) { if (!loadQPD(libQPDPath, libpythonPath)) { llvm::errs() << "failed to load libQuantumPythonCallbacks\n"; @@ -365,8 +367,6 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasesetName((outOp->getName() + "_" + pauliWord).str()); // unique name per pauliword funcOp->setAttr("target_gate", mlir::StringAttr::get(context, "paulirot" + pauliWord)); - auto analysis = ResourceAnalysis(funcOp); - if (const ResourceResult *flat = analysis.getFlattenedResource(funcOp.getName())) { - funcOp->setAttr("resources", buildResourceDict(context, *flat)); - } - if (failed(addRuleNode(funcOp, ruleNodes))) { return failure(); } @@ -524,13 +519,10 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &rules, llvm::StringSet<> &userRuleNames) { - LDBG() << "getting rule nodes"; - // Load pre-compiled rules (ignore failure, we can try to solve without) std::ignore = loadBuiltInDecompositionRules(filename, rules); // Lower and load compile-time rules - LDBG() << "loading paulirot rules"; if (failed(loadPauliRotRules(rules))) { return failure(); } @@ -580,7 +572,8 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase Date: Fri, 26 Jun 2026 15:18:32 -0400 Subject: [PATCH 12/78] changelog --- doc/releases/changelog-dev.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c990c2ad88..4f127d6e32 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -5,6 +5,13 @@

Improvements 🛠

+* The `decompose-lowering` pass now supports applying a selection of the available decomposition rules via the `target_rules` parameter. + The pass also no longer applies the `inline`, `cse` and `canonicalize` passes to avoid unnecessary IR mutations. + Instead, decomposition rules are deterministically inlined by a custom function (`inline` is non-deterministic, using an estimated benefit and threshold as criteria for inlining). + Decomposition rules are no longer removed after the `decompose-lowering` pass, which allows them to be used by subsequent passes, namely `graph-decomposition`. + Instead, rules are removed by the `symbol-dce` pass at the end of the `QuantumCompilationStage`. + [(#2973)](https://github.com/PennyLaneAI/catalyst/pull/2973) + * The `ResourceAnalysis` pass now reports each loop body and each subroutine as its own entry instead of folding their gate counts into the caller. Loops with constant bounds appear as `for_loop_` with their trip count. Loops with dynamic bounds appear as `dyn_for_loop_` with a stable From 30de0cad05d25533b2c4a366b2501eafd7c3bb6e Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 26 Jun 2026 15:25:28 -0400 Subject: [PATCH 13/78] changelog --- doc/releases/changelog-dev.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 4f127d6e32..996b3eeea6 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -201,6 +201,9 @@

Internal changes ⚙️

+* The `graph-decomposition` pass now performs far less IR manipulation. + [(#2977)](https://github.com/PennyLaneAI/catalyst/pull/2977) + * Update tests to not use global capture toggle where possible. [(#2964)](https://github.com/PennyLaneAI/catalyst/pull/2964) From 73e6de0ae9a00138378b63e69ca401cbfb045996 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Mon, 29 Jun 2026 16:08:47 -0400 Subject: [PATCH 14/78] define decomp interface --- mlir/include/Quantum/IR/QuantumInterfaces.td | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index 01e49f2c13..294aa4c3a0 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -179,6 +179,28 @@ def DifferentiableGate : OpInterface<"DifferentiableGate", [ParametrizedGate]> { ]; } +def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { + let description = [{ + This interface provides a generic way to query decomposition data from a quantum operation. + }]; + + let cppNamespace = "::catalyst::quantum"; + + let methods = [ + InterfaceMethod< + "Return the shape and dtype of dynamic data.", + "mlir::TypeRange", "getDynamicShape" + >, + InterfaceMethod< + "Return the values of static data.", + "mlir::DictionaryAttr", "getStaticData" + >, + InterfaceMethod< + "Return the operation name.", "std::string", "getDecompId" + > + ]; +} + def MeasurementProcess : OpInterface<"MeasurementProcess"> { let description = [{ This interface provides a generic way to interact with quantum measurement processes. From eb637defc620b8f38c706a935251966b4c38f6ca Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Mon, 29 Jun 2026 16:09:00 -0400 Subject: [PATCH 15/78] add interface to paulirot --- mlir/include/Quantum/IR/QuantumOps.td | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 8623922262..28d99afef6 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -689,7 +689,8 @@ def OperatorOp : UnitaryGate_Op<"operator", [ParametrizedGate, NoMemoryEffect, } def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, - AttrSizedOperandSegments, AttrSizedResultSegments]> { + AttrSizedOperandSegments, AttrSizedResultSegments, + DecomposableGate]> { let summary = "Apply a Pauli Product Rotation"; let description = [{ The `quantum.paulirot` operation applies a rotation around a Pauli product @@ -729,6 +730,20 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, } return pauliWord; } + + std::string getDecompId() { + return "paulirot" + getPauliWord(); + } + + mlir::TypeRange getDynamicShape() { + return getAllParams().getTypes(); + } + + mlir::DictionaryAttr getStaticData() { + mlir::MLIRContext *ctx = getContext(); + mlir::NamedAttribute pauliWordEntry = mlir::NamedAttribute(mlir::StringAttr::get(ctx, "pauli_word"), getPauliProduct()); + return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); + } }]; let hasVerifier = 1; From 99d4a7afa8bc8029d0ce77b034e7ba5f25a38667 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 30 Jun 2026 11:31:38 -0400 Subject: [PATCH 16/78] update interface --- mlir/include/Quantum/IR/QuantumInterfaces.td | 8 +++++++- mlir/include/Quantum/IR/QuantumOps.td | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index 294aa4c3a0..f367249d7c 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -196,7 +196,13 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { "mlir::DictionaryAttr", "getStaticData" >, InterfaceMethod< - "Return the operation name.", "std::string", "getDecompId" + "Return the operations ID for use with the graph.", "std::string", "getDecompId" + >, + InterfaceMethod< + "Return the name of the operation.", "std::string", "getOpName" + >, + InterfaceMethod< + "Return the number of wires the operation acts on.", "size_t", "getNumWires" > ]; } diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 28d99afef6..7b537a0e7a 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -735,15 +735,24 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, return "paulirot" + getPauliWord(); } + std::string getOpName() { + return "paulirot"; + } + mlir::TypeRange getDynamicShape() { return getAllParams().getTypes(); } mlir::DictionaryAttr getStaticData() { mlir::MLIRContext *ctx = getContext(); - mlir::NamedAttribute pauliWordEntry = mlir::NamedAttribute(mlir::StringAttr::get(ctx, "pauli_word"), getPauliProduct()); + mlir::NamedAttribute pauliWordEntry = mlir::NamedAttribute(mlir::StringAttr::get(ctx, "pauli_word"), mlir::StringAttr::get(ctx, getPauliWord())); return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); } + + size_t getNumWires() { + return getNonCtrlQubitOperands().size(); + } + }]; let hasVerifier = 1; From 6b12ac0a8291682aa965034295fdbb633b36845f Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 30 Jun 2026 11:37:39 -0400 Subject: [PATCH 17/78] generalize python rule lowering --- mlir/include/Quantum/Transforms/QPDLoader.h | 7 +- mlir/lib/Quantum/IR/QuantumOps.cpp | 24 ++++++ mlir/lib/Quantum/Transforms/QPDLoader.cpp | 20 ++--- .../PythonFunction.cpp | 77 +++++++++++++++++-- .../Transforms/graph_decomposition.cpp | 48 ++++++------ 5 files changed, 130 insertions(+), 46 deletions(-) diff --git a/mlir/include/Quantum/Transforms/QPDLoader.h b/mlir/include/Quantum/Transforms/QPDLoader.h index 94e94d7c96..4e29d556f7 100644 --- a/mlir/include/Quantum/Transforms/QPDLoader.h +++ b/mlir/include/Quantum/Transforms/QPDLoader.h @@ -17,12 +17,13 @@ #include #include +#include "Quantum/IR/QuantumInterfaces.h" + namespace catalyst::quantum { -using LowerPauliRotFn = std::string (*)(double theta, std::string pauliWord, - std::vector wires); +using PythonRuleLoweringFn = std::string (*)(DecomposableGate op); -extern LowerPauliRotFn pythonLowerPauliRot; +extern PythonRuleLoweringFn pythonRuleLowering; bool loadQPD(std::string libQPDPath, std::string libpythonPath); diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 62977102c4..e0ee542d6f 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -14,21 +14,45 @@ #include "Quantum/IR/QuantumOps.h" +#include +#include +#include #include +#include #include #include #include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/Block.h" #include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OpDefinition.h" #include "mlir/IR/OpImplementation.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/TypeRange.h" +#include "mlir/IR/Value.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Support/WalkResult.h" #include "QRef/IR/QRefOps.h" #include "Quantum/IR/QuantumAttrDefs.h" #include "Quantum/IR/QuantumDialect.h" +#include "Quantum/IR/QuantumInterfaces.h" +#include "Quantum/IR/QuantumTypes.h" using namespace mlir; using namespace catalyst::quantum; diff --git a/mlir/lib/Quantum/Transforms/QPDLoader.cpp b/mlir/lib/Quantum/Transforms/QPDLoader.cpp index 2f2b47f3e1..3df3979b94 100644 --- a/mlir/lib/Quantum/Transforms/QPDLoader.cpp +++ b/mlir/lib/Quantum/Transforms/QPDLoader.cpp @@ -29,7 +29,7 @@ namespace catalyst::quantum { // TODO: genericize this -LowerPauliRotFn pythonLowerPauliRot = nullptr; +PythonRuleLoweringFn pythonRuleLowering = nullptr; namespace { @@ -131,7 +131,7 @@ void ensureLibpythonLoaded(std::string libpythonPath) "library path to prevent this.\n"; } -LowerPauliRotFn loadAndResolve(std::string libQPDPath, std::string libpythonPath) +PythonRuleLoweringFn loadAndResolve(std::string libQPDPath, std::string libpythonPath) { std::string path = resolvePluginPath(libQPDPath); if (path.empty()) { @@ -153,18 +153,18 @@ LowerPauliRotFn loadAndResolve(std::string libQPDPath, std::string libpythonPath // use a c-safe function for getting the lowering function to allow cpp types in the python // caller auto *getLoweringFunction = - reinterpret_cast(::dlsym(libHandle, "getPythonLowerPauliRot")); + reinterpret_cast(::dlsym(libHandle, "getPythonRuleLoweringFunction")); if (!getLoweringFunction) { llvm::errs() << "[QPD-loader] dlopen succeeded but symbol " - "'getPythonLowerPauliRot' not found in '" + "'getPythonRuleLoweringFunction' not found in '" << path << "'\n"; } // resolve the proper python-decomposition lowering function via the getter - auto *sym = reinterpret_cast(getLoweringFunction()); + auto *sym = reinterpret_cast(getLoweringFunction()); if (!sym) { llvm::errs() << "[QPD-loader] dlopen succeeded but symbol " - "'pythonLowerPauliRot' not found in '" + "'pythonRuleLoweringFunction' not found in '" << path << "'\n"; } return sym; @@ -174,19 +174,19 @@ LowerPauliRotFn loadAndResolve(std::string libQPDPath, std::string libpythonPath bool loadQPD(std::string libQPDPath, std::string libpythonPath) { - if (pythonLowerPauliRot) { + if (pythonRuleLowering) { return true; } // Avoid additional lookups if (resolutionAttempted.exchange(true)) { // explicit check in case of thread race conditions - return pythonLowerPauliRot != nullptr; + return pythonRuleLowering != nullptr; } - pythonLowerPauliRot = loadAndResolve(libQPDPath, libpythonPath); + pythonRuleLowering = loadAndResolve(libQPDPath, libpythonPath); - return pythonLowerPauliRot != nullptr; + return pythonRuleLowering != nullptr; } } // namespace catalyst::quantum diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index ceae08ca01..392f78b217 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -12,37 +12,100 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include #include #include +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/raw_ostream.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/TypeRange.h" #include "nanobind/nanobind.h" #include "nanobind/stl/string.h" // for automatic string conversion #include "nanobind/stl/vector.h" // for automatic vector conversion +#include "Quantum/IR/QuantumInterfaces.h" +#include "Quantum/IR/QuantumOps.h" + #include "PythonDriverUtils.hpp" #define DEBUG_TYPE "[QPD] " namespace nb = nanobind; -std::string pythonLowerPauliRot(double theta, const std::string &pauliWord, std::vector wires) +nb::object getPyvalFromTypeRange(mlir::TypeRange typerange) +{ + nb::list pyTypes; + for (auto type : typerange) { + std::string typestr; + llvm::raw_string_ostream ss(typestr); + type.print(ss); + pyTypes.append(typestr); + } + return pyTypes; +} + +nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) { + return llvm::TypeSwitch(attr) + .Case([](auto dictAttr) { + nb::dict outDict; + for (auto namedAttr : dictAttr) { + outDict[namedAttr.getName().str().c_str()] = + getPyvalFromMlirAttribute(namedAttr.getValue()); + } + return outDict; + }) + .Case([](auto arrAttr) { + nb::list outTuple; + for (auto val : arrAttr) { + outTuple.append(getPyvalFromMlirAttribute(val)); + } + return outTuple; + }) + .Case([](auto strAttr) { return nb::cast(strAttr.getValue().str()); }) + .Default([](auto attr) { return nb::cast("placeholder"); }); +} + +std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) +{ + std::cout << "using new python lowering for op with the following data:\n"; + std::cout << "op ID: " << op.getDecompId() << "\n"; + + std::cout << "dynamic data shape:\n"; + for (auto type : op.getDynamicShape()) { + type.dump(); + } + + std::cout << "static data:\n"; + for (auto namedAttr : op.getStaticData()) { + std::cout << namedAttr.getName().getValue().str(); + namedAttr.getValue().dump(); + std::cout << "\n"; + } + + std::cout << "trying nanobind now\n"; + QuantumPythonDecompositions::PyInterpreterGuard guard; std::string mlirText = guard.withGil([&] -> std::string { const char *moduleName = "catalyst.device.python_decompositions"; - const char *functionName = "paulirot_decomposition_wrapper"; + const char *functionName = "python_decomposition_wrapper"; try { nb::module_ wrapperModule = nb::module_::import_(moduleName); nb::object wrapperFunction = wrapperModule.attr(functionName); - nb::object pythonResult = wrapperFunction(theta, pauliWord, wires); + nb::object pythonResult = wrapperFunction( + op.getOpName(), op.getDecompId(), getPyvalFromTypeRange(op.getDynamicShape()), + getPyvalFromMlirAttribute(op.getStaticData()), op.getNumWires()); return nb::borrow(pythonResult).c_str(); } catch (const nb::python_error &error) { - throw QuantumPythonDecompositions::TracingError(moduleName, functionName, pauliWord, - error.what()); + throw QuantumPythonDecompositions::TracingError(moduleName, functionName, + op.getDecompId(), error.what()); } catch (const std::exception &error) { throw; @@ -52,7 +115,7 @@ std::string pythonLowerPauliRot(double theta, const std::string &pauliWord, std: return mlirText; } -extern "C" __attribute__((visibility("default"))) void *getPythonLowerPauliRot() +extern "C" __attribute__((visibility("default"))) void *getPythonRuleLoweringFunction() { - return reinterpret_cast(pythonLowerPauliRot); + return reinterpret_cast(pythonRuleLowering); } diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index c1ec170efb..addba0d93f 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -338,47 +338,42 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &ruleNodes) + mlir::LogicalResult loadPythonDecomps(std::vector &ruleNodes) { mlir::ModuleOp module = getOperation(); MLIRContext *context = &getContext(); - llvm::StringSet<> addedWords; - // Add words from existing paulirot rules + llvm::StringSet<> handledOpIds; + // Add IDs from existing decomposable ops module.walk([&](mlir::func::FuncOp func) { if (func->hasAttr("target_gate")) { - if (func.getName().starts_with("paulirot_decomp_rule_")) { - addedWords.insert(func.getName().drop_front(21)); - } + handledOpIds.insert(func->getAttrOfType("target_gate").str()); } }); - llvm::SmallVector pauliRotOps; - module.walk([&](quantum::PauliRotOp op) { pauliRotOps.push_back(op); }); + llvm::SmallVector decomposableOps; + module.walk([&](quantum::DecomposableGate op) { decomposableOps.push_back(op); }); - if (!pauliRotOps.empty()) { + if (!decomposableOps.empty()) { if (!loadQPD(libQPDPath, libpythonPath)) { llvm::errs() << "failed to load libQuantumPythonCallbacks\n"; return failure(); } } - for (quantum::PauliRotOp pauliRot : pauliRotOps) { - std::string pauliWord = pauliRot.getPauliWord(); + for (quantum::DecomposableGate op : decomposableOps) { + std::string opId = op.getDecompId(); + std::string ruleName = opId + "_decomposition"; - if (addedWords.contains(pauliWord)) { + if (handledOpIds.contains(opId)) { continue; } - addedWords.insert(pauliWord); - - std::vector wires(pauliRot.getInQubits().size()); - std::iota(wires.begin(), wires.end(), 0); + handledOpIds.insert(opId); - std::string mlirText = pythonLowerPauliRot(0.2, pauliWord, wires); + std::string mlirText = pythonRuleLowering(op); mlir::ParserConfig config(context); auto moduleOp = mlir::parseSourceString(llvm::StringRef(mlirText), config); @@ -390,7 +385,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase outOp; moduleOp->walk([&](mlir::func::FuncOp func) { // TODO: enable multiple decomposition rules for the same operator - if (func.getName() == "paulirot_decomp_rule") { + if (func.getName() == ruleName) { func->remove(); outOp = mlir::OwningOpRef(func); return mlir::WalkResult::interrupt(); @@ -399,18 +394,18 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasesetName((outOp->getName() + "_" + pauliWord).str()); // unique name per pauliword - funcOp->setAttr("target_gate", mlir::StringAttr::get(context, "paulirot" + pauliWord)); + outOp->setName(ruleName); // unique name per pauliword + funcOp->setAttr("target_gate", mlir::StringAttr::get(context, opId)); if (failed(addRuleNode(funcOp, ruleNodes))) { return failure(); } - LDBG() << "adding rule " << funcOp.getName(); + LDBG() << "adding rule " << ruleName; module.push_back(std::move(outOp.release())); } return success(); @@ -444,6 +439,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasegetName().stripDialect().str(); if (name == "gphase") { name = "GlobalPhase"; @@ -523,7 +519,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase Date: Tue, 30 Jun 2026 11:49:04 -0400 Subject: [PATCH 18/78] generalize frontend --- .../catalyst/device/python_decompositions.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index b0c538ccd4..e123fdc582 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -42,20 +42,21 @@ import pennylane as qp -def paulirot_decomposition_wrapper(theta, pauli_word, wires): - """Wraps the paulirot decomp rule for compile-time lowering with a static pauli word. +def python_decomposition_wrapper(op_name, op_id, dynamic_shape, static_data, num_wires) -> str: + """Generic decomposition wrapper.""" + print("Hello from python! Here's the received data:", op_name, dynamic_shape, static_data) - The decomposition rule is identifiable by the name `paulirot_decomp_rule`. - """ - device = qp.device("null.qubit", wires=len(wires)) - wires = jnp.array(wires) + device = qp.device("null.qubit", wires=num_wires) + wires = jnp.array(range(num_wires)) - def paulirot_decomp_rule(theta, wires): + def decomp_rule(*params, wires): qp.ops.qubit.parametric_ops_multi_qubit._pauli_rot_decomposition._impl( - theta, wires, pauli_word + *params, wires=wires, **static_data ) - paulirot_subroutine = qp.capture.subroutine(paulirot_decomp_rule) + decomp_rule.__name__ = op_id + "_decomposition" + + paulirot_subroutine = qp.capture.subroutine(decomp_rule) @qp.qjit( target="mlir", @@ -63,6 +64,6 @@ def paulirot_decomp_rule(theta, wires): ) @qp.qnode(device=device) def circuit(): - paulirot_subroutine(theta, wires) + paulirot_subroutine(*[0.5 for _ in dynamic_shape], wires=wires) return str(circuit.mlir_module) From bca78019175aa40a31f900a6c102637b7c77e2b5 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Thu, 2 Jul 2026 10:25:40 -0400 Subject: [PATCH 19/78] support multiple rules --- .../catalyst/device/python_decompositions.py | 20 +++++++---- frontend/test/pytest/test_QPD.py | 29 +++++++++++++-- mlir/include/Quantum/IR/QuantumInterfaces.td | 11 ++++-- mlir/include/Quantum/IR/QuantumOps.td | 8 +++-- .../Transforms/graph_decomposition.cpp | 36 ++++++++----------- 5 files changed, 68 insertions(+), 36 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index e123fdc582..6f49285564 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -47,16 +47,21 @@ def python_decomposition_wrapper(op_name, op_id, dynamic_shape, static_data, num print("Hello from python! Here's the received data:", op_name, dynamic_shape, static_data) device = qp.device("null.qubit", wires=num_wires) + # TODO support multiple wire args/qregs wires = jnp.array(range(num_wires)) - def decomp_rule(*params, wires): - qp.ops.qubit.parametric_ops_multi_qubit._pauli_rot_decomposition._impl( - *params, wires=wires, **static_data - ) + def rule_to_subroutine(rule): + def decomp_rule(*params, wires): + rule._impl(*params, wires=wires, **static_data) - decomp_rule.__name__ = op_id + "_decomposition" + decomp_rule.__name__ = op_id + "_" + rule.name - paulirot_subroutine = qp.capture.subroutine(decomp_rule) + return qp.capture.subroutine(decomp_rule) + + # let this fail with the standard error message if the op is not found + op_class = getattr(qp, op_name) + + subroutines = [rule_to_subroutine(rule) for rule in qp.decomposition.list_decomps(op_class)] @qp.qjit( target="mlir", @@ -64,6 +69,7 @@ def decomp_rule(*params, wires): ) @qp.qnode(device=device) def circuit(): - paulirot_subroutine(*[0.5 for _ in dynamic_shape], wires=wires) + for subroutine in subroutines: + subroutine(*[0.5 for _ in dynamic_shape], wires=wires) return str(circuit.mlir_module) diff --git a/frontend/test/pytest/test_QPD.py b/frontend/test/pytest/test_QPD.py index 8929116fbd..10683d634b 100644 --- a/frontend/test/pytest/test_QPD.py +++ b/frontend/test/pytest/test_QPD.py @@ -14,9 +14,10 @@ """Unit tests for the python decompositions module.""" +import pennylane as qp import pytest -from catalyst.device.python_decompositions import paulirot_decomposition_wrapper +from catalyst.device.python_decompositions import python_decomposition_wrapper class TestQPD: @@ -24,12 +25,34 @@ class TestQPD: def test_paulirot_wrapper(self): """Test that the paulirot QPD wrapper correctly returns the IR as a string.""" - result = paulirot_decomposition_wrapper(0.4, "XZZ", [0, 1, 2]) + result = python_decomposition_wrapper( + "PauliRot", "paulirotXZZ", [0.4], {"pauli_word": "XZZ"}, 3 + ) assert isinstance(result, str) - assert "paulirot_decomp_rule" in result + assert "paulirotXZZ__pauli_rot_decomposition" in result assert "Hadamard" in result assert "multirz" in result + def test_multiple_rules(self): + """Test that the python decomposition wrapper supports multiple rules.""" + with qp.decomposition.local_decomps(): + + def test_resources(): + return {qp.X: 1} + + @qp.register_resources(test_resources) + def test_decomp(angle, wires, pauli_word): + qp.RX(angle, wires[0]) + + qp.add_decomps(qp.PauliRot, test_decomp) + + result = python_decomposition_wrapper( + "PauliRot", "paulirotXYX", [float], {"pauli_word": "XYX"}, 3 + ) + + assert "paulirotXYX_test_decomp" in result + assert "test_decomp" in result + if __name__ == "__main__": pytest.main(["-x", __file__]) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index f367249d7c..33293ac056 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -196,13 +196,18 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { "mlir::DictionaryAttr", "getStaticData" >, InterfaceMethod< - "Return the operations ID for use with the graph.", "std::string", "getDecompId" + "Return the operation's ID for use with the graph." + "This should be a unique combination of the name and the static data associated with" + " the op.", + "std::string", "getDecompId" >, InterfaceMethod< - "Return the name of the operation.", "std::string", "getOpName" + "Return the name of the corresponding PennyLane operator.", "std::string", "getOpName" >, + // TODO: change this to vector for multiple registers/wire args InterfaceMethod< - "Return the number of wires the operation acts on.", "size_t", "getNumWires" + "Return the number of wires the operation acts on for each wire argument.", + "size_t", "getNumWires" > ]; } diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 7b537a0e7a..f6b7b01fc3 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -736,7 +736,7 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, } std::string getOpName() { - return "paulirot"; + return "PauliRot"; } mlir::TypeRange getDynamicShape() { @@ -745,7 +745,11 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, mlir::DictionaryAttr getStaticData() { mlir::MLIRContext *ctx = getContext(); - mlir::NamedAttribute pauliWordEntry = mlir::NamedAttribute(mlir::StringAttr::get(ctx, "pauli_word"), mlir::StringAttr::get(ctx, getPauliWord())); + mlir::NamedAttribute pauliWordEntry = + mlir::NamedAttribute( + mlir::StringAttr::get(ctx, "pauli_word"), + mlir::StringAttr::get(ctx, getPauliWord()) + ); return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); } diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index addba0d93f..1c93a671d7 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -347,7 +347,10 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase handledOpIds; - // Add IDs from existing decomposable ops + // Add IDs from existing decomposable ops with decomposition rules + // NOTE: we assume in general that if one decomposition rule for an op is available, then + // all decomposition rules for that op are available. No system should introduce a subset of + // the rules for an op. module.walk([&](mlir::func::FuncOp func) { if (func->hasAttr("target_gate")) { handledOpIds.insert(func->getAttrOfType("target_gate").str()); @@ -366,7 +369,6 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase outOp; moduleOp->walk([&](mlir::func::FuncOp func) { - // TODO: enable multiple decomposition rules for the same operator - if (func.getName() == ruleName) { + if (func.getName().starts_with(opId)) { + mlir::OwningOpRef outOp; func->remove(); outOp = mlir::OwningOpRef(func); - return mlir::WalkResult::interrupt(); + mlir::func::FuncOp funcOp = outOp.get(); + funcOp->setAttr("target_gate", mlir::StringAttr::get(context, opId)); + + // if we fail to add one of the decomps, we still want to try for the rest + std::ignore = addRuleNode(funcOp, ruleNodes); + LDBG() << "adding rule " << funcOp.getName(); + module.push_back(std::move(outOp.release())); } return mlir::WalkResult::advance(); }); - - if (!outOp) { - llvm::errs() << "failed to find " << ruleName << " in parsed MLIR\n"; - return failure(); - } - - mlir::func::FuncOp funcOp = outOp.get(); - outOp->setName(ruleName); // unique name per pauliword - funcOp->setAttr("target_gate", mlir::StringAttr::get(context, opId)); - - if (failed(addRuleNode(funcOp, ruleNodes))) { - return failure(); - } - LDBG() << "adding rule " << ruleName; - module.push_back(std::move(outOp.release())); } return success(); } From 4023f9eb654e300ccaac112a7ef96f707f30b61e Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 7 Jul 2026 10:28:20 -0400 Subject: [PATCH 20/78] Update doc/releases/changelog-dev.md Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0bc19155f7..5956165108 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -205,7 +205,9 @@

Internal changes ⚙️

-* The `graph-decomposition` pass now performs far less IR manipulation. +* The `graph-decomposition` pass eliminates three redundant IR manipulations: + the cloning, removal, and re-insertion of user rules. This optimization is particularly + beneficial when the pass is executed multiple times within the compilation pipeline. [(#2977)](https://github.com/PennyLaneAI/catalyst/pull/2977) * Update tests to not use global capture toggle where possible. From 24237a01338458edeaa35357bd70732163bc0991 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 7 Jul 2026 11:37:24 -0400 Subject: [PATCH 21/78] Apply suggestion from @maliasadi Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> --- mlir/lib/Quantum/Transforms/graph_decomposition.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index c1ec170efb..f9b862d7d8 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -411,7 +411,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase Date: Tue, 7 Jul 2026 11:53:29 -0400 Subject: [PATCH 22/78] review changes --- .../Quantum/Transforms/graph_decomposition.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index f9b862d7d8..0578b99b8a 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -242,7 +242,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasegetAttrOfType("resources"); } + // Fail if resources are missing + if (!resourcesAttr) { + llvm::errs() << "Decomposition rule " << ruleName + << " was provided without resources, and resources could not be generated " + "for it.\n"; + return failure(); + } + // 2. Extract 'operations' dictionary from resources auto operations = mlir::dyn_cast_or_null(resourcesAttr.get("operations")); if (!operations) { @@ -338,9 +346,10 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &ruleNodes) { @@ -350,6 +359,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase addedWords; // Add words from existing paulirot rules module.walk([&](mlir::func::FuncOp func) { + // TODO: generalize this if (func->hasAttr("target_gate")) { if (func.getName().starts_with("paulirot_decomp_rule_")) { addedWords.insert(func.getName().drop_front(21)); From 4edaf7ca359ac410e830a21df2d5afcbe9f23be7 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 8 Jul 2026 10:26:13 -0400 Subject: [PATCH 23/78] fix merge mistake --- mlir/lib/Quantum/Transforms/graph_decomposition.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index 5ffe84299b..0578b99b8a 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -156,15 +156,6 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(rule->getName())) { - module.getBody()->push_back(rule.release()); - } - } } private: From 35cfc99f396f9fca9589c655d6136421b4a604e0 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 8 Jul 2026 13:28:54 -0400 Subject: [PATCH 24/78] move interface methods to cpp file --- mlir/include/Quantum/IR/QuantumInterfaces.td | 27 ++++++++++------- mlir/include/Quantum/IR/QuantumOps.td | 32 ++------------------ mlir/lib/Quantum/IR/QuantumOps.cpp | 20 ++++++++++++ 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index 33293ac056..4b35a2f356 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -181,7 +181,7 @@ def DifferentiableGate : OpInterface<"DifferentiableGate", [ParametrizedGate]> { def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { let description = [{ - This interface provides a generic way to query decomposition data from a quantum operation. + This interface provides a generic way to query decomposition data from a quantum operation. An operation must implement this interface in order to compatible with the `graph-decomposition` pass. Note that implementing this interface does not guarantee that an operation will decomposed successfully, only that it will be registered with the graph. }]; let cppNamespace = "::catalyst::quantum"; @@ -192,22 +192,27 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { "mlir::TypeRange", "getDynamicShape" >, InterfaceMethod< - "Return the values of static data.", - "mlir::DictionaryAttr", "getStaticData" + "Return the number of wires the operation acts on for each wire argument.", + "std::vector", "getWireLens" >, InterfaceMethod< - "Return the operation's ID for use with the graph." - "This should be a unique combination of the name and the static data associated with" - " the op.", - "std::string", "getDecompId" + "Return the values of static data.", + "mlir::DictionaryAttr", "getStaticData" >, InterfaceMethod< - "Return the name of the corresponding PennyLane operator.", "std::string", "getOpName" + "Return the name of the corresponding PennyLane operator.", "std::string", "getPlName" >, - // TODO: change this to vector for multiple registers/wire args InterfaceMethod< - "Return the number of wires the operation acts on for each wire argument.", - "size_t", "getNumWires" + "Return the operation's ID for use with the graph. " + "This should be computed by the following convention:\n" + "```\n" + "if uid:\n" + " uid\n" + "else:\n" + " name + {dtype tree of args} + {number of wires per wires arg} + {static data}\n" + "```\n" + "Note that UID is currently specific to `OperatorOp`s.", + "std::string", "getDecompId" > ]; } diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index f6b7b01fc3..9f0d09f0d8 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -690,7 +690,7 @@ def OperatorOp : UnitaryGate_Op<"operator", [ParametrizedGate, NoMemoryEffect, def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, AttrSizedOperandSegments, AttrSizedResultSegments, - DecomposableGate]> { + DeclareOpInterfaceMethods]> { let summary = "Apply a Pauli Product Rotation"; let description = [{ The `quantum.paulirot` operation applies a rotation around a Pauli product @@ -722,7 +722,8 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, return getODSOperands(getParamOperandIdx()); } - std::string getPauliWord() { + std::string getPauliWord() + { std::string pauliWord; for (mlir::Attribute pauliChar : getPauliProduct()) { @@ -730,33 +731,6 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, } return pauliWord; } - - std::string getDecompId() { - return "paulirot" + getPauliWord(); - } - - std::string getOpName() { - return "PauliRot"; - } - - mlir::TypeRange getDynamicShape() { - return getAllParams().getTypes(); - } - - mlir::DictionaryAttr getStaticData() { - mlir::MLIRContext *ctx = getContext(); - mlir::NamedAttribute pauliWordEntry = - mlir::NamedAttribute( - mlir::StringAttr::get(ctx, "pauli_word"), - mlir::StringAttr::get(ctx, getPauliWord()) - ); - return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); - } - - size_t getNumWires() { - return getNonCtrlQubitOperands().size(); - } - }]; let hasVerifier = 1; diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index e0ee542d6f..3a48bdc10a 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1160,3 +1160,23 @@ ParseResult OperatorOp::parse(OpAsmParser &parser, OperationState &result) return success(); } + +//===----------------------------------------------------------------------===// +// Quantum op interface methods. +//===----------------------------------------------------------------------===// + +mlir::TypeRange PauliRotOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::string PauliRotOp::getDecompId() { return "paulirot" + getPauliWord(); } + +std::string PauliRotOp::getPlName() { return "PauliRot"; } + +mlir::DictionaryAttr PauliRotOp::getStaticData() +{ + mlir::MLIRContext *ctx = getContext(); + mlir::NamedAttribute pauliWordEntry = mlir::NamedAttribute( + mlir::StringAttr::get(ctx, "pauli_word"), mlir::StringAttr::get(ctx, getPauliWord())); + return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); +} + +std::vector PauliRotOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } From c4d2091e054b0a1178288ffdbb3aa8639a5c83ad Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 8 Jul 2026 13:29:11 -0400 Subject: [PATCH 25/78] support multiple wire args --- .../catalyst/device/python_decompositions.py | 18 ++++++++++++------ .../PythonFunction.cpp | 11 +++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 6f49285564..68f29ea980 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -42,18 +42,24 @@ import pennylane as qp -def python_decomposition_wrapper(op_name, op_id, dynamic_shape, static_data, num_wires) -> str: +def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, static_data) -> str: """Generic decomposition wrapper.""" - print("Hello from python! Here's the received data:", op_name, dynamic_shape, static_data) + print( + "Hello from python! Here's the received data:", + op_name, + dynamic_shape, + wire_lens, + static_data, + ) - device = qp.device("null.qubit", wires=num_wires) - # TODO support multiple wire args/qregs - wires = jnp.array(range(num_wires)) + device = qp.device("null.qubit", wires=sum(wire_lens)) + wires = tuple(jnp.array(range(length)) for length in wire_lens) def rule_to_subroutine(rule): def decomp_rule(*params, wires): - rule._impl(*params, wires=wires, **static_data) + rule._impl(*params, *wires, **static_data) + # TODO remove this once we have unified lowering, we should be able to set target_gate decomp_rule.__name__ = op_id + "_" + rule.name return qp.capture.subroutine(decomp_rule) diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index 392f78b217..5fe9b70d6a 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -72,6 +72,7 @@ nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) { std::cout << "using new python lowering for op with the following data:\n"; + std::cout << "op name: " << op.getPlName() << "\n"; std::cout << "op ID: " << op.getDecompId() << "\n"; std::cout << "dynamic data shape:\n"; @@ -79,6 +80,12 @@ std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) type.dump(); } + std::cout << "wire lens:\n"; + for (size_t len : op.getWireLens()) { + std::cout << len << ", "; + } + std::cout << "\n"; + std::cout << "static data:\n"; for (auto namedAttr : op.getStaticData()) { std::cout << namedAttr.getName().getValue().str(); @@ -98,8 +105,8 @@ std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) nb::object wrapperFunction = wrapperModule.attr(functionName); nb::object pythonResult = wrapperFunction( - op.getOpName(), op.getDecompId(), getPyvalFromTypeRange(op.getDynamicShape()), - getPyvalFromMlirAttribute(op.getStaticData()), op.getNumWires()); + op.getPlName(), op.getDecompId(), getPyvalFromTypeRange(op.getDynamicShape()), + op.getWireLens(), getPyvalFromMlirAttribute(op.getStaticData())); return nb::borrow(pythonResult).c_str(); } From 2a2fd76711a2b432129ae4ff7b1abb4b8a93f4e6 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 09:06:19 -0400 Subject: [PATCH 26/78] improve interface implementation --- mlir/include/Quantum/IR/QuantumInterfaces.h | 13 +++- mlir/include/Quantum/IR/QuantumInterfaces.td | 13 ++-- mlir/include/Quantum/IR/QuantumOps.td | 3 +- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 75 ++++++++++++++++++- mlir/lib/Quantum/IR/QuantumOps.cpp | 45 ++++++++++- .../PythonFunction.cpp | 10 +-- .../Transforms/graph_decomposition.cpp | 2 +- 7 files changed, 139 insertions(+), 22 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.h b/mlir/include/Quantum/IR/QuantumInterfaces.h index d19ebbcfa1..777f8d115c 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.h +++ b/mlir/include/Quantum/IR/QuantumInterfaces.h @@ -14,10 +14,21 @@ #pragma once -#include "mlir/IR/OpDefinition.h" +#include + +#include "mlir/IR/OpDefinition.h" // for QuantumInterfaces.h..inc +#include "mlir/IR/Operation.h" //===----------------------------------------------------------------------===// // Quantum interface declarations. //===----------------------------------------------------------------------===// +namespace catalyst { +namespace quantum { + +std::string defaultGetGraphOpId(mlir::Operation *op); + +} +} // namespace catalyst + #include "Quantum/IR/QuantumInterfaces.h.inc" diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index 4b35a2f356..ca91f975ff 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -187,6 +187,9 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { let cppNamespace = "::catalyst::quantum"; let methods = [ + InterfaceMethod< + "Return the name of the corresponding PennyLane operator.", "std::string", "getPlName" + >, InterfaceMethod< "Return the shape and dtype of dynamic data.", "mlir::TypeRange", "getDynamicShape" @@ -199,20 +202,14 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { "Return the values of static data.", "mlir::DictionaryAttr", "getStaticData" >, - InterfaceMethod< - "Return the name of the corresponding PennyLane operator.", "std::string", "getPlName" - >, InterfaceMethod< "Return the operation's ID for use with the graph. " "This should be computed by the following convention:\n" "```\n" - "if uid:\n" - " uid\n" - "else:\n" - " name + {dtype tree of args} + {number of wires per wires arg} + {static data}\n" + "name + {dtype tree of args} + {number of wires per wires arg} + {static data} + {uid}\n" "```\n" "Note that UID is currently specific to `OperatorOp`s.", - "std::string", "getDecompId" + "std::string", "getGraphOpId", (ins), [{}], [{ return ::catalyst::quantum::defaultGetGraphOpId($_op.getOperation()); }] > ]; } diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 9f0d09f0d8..64c6a0db9b 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -535,7 +535,8 @@ def CustomOp : UnitaryGate_Op<"custom", [DifferentiableGate, NoMemoryEffect, } def OperatorOp : UnitaryGate_Op<"operator", [ParametrizedGate, NoMemoryEffect, - AttrSizedOperandSegments, AttrSizedResultSegments]> { + AttrSizedOperandSegments, AttrSizedResultSegments, + DeclareOpInterfaceMethods]> { let summary = "A generalized quantum operation with arbitrary input signature."; let description = [{ The `quantum.operator` is a generic representation for arbitrary quantum operations diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index a8a120fef9..dfd57f9769 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -14,11 +14,84 @@ #include "Quantum/IR/QuantumInterfaces.h" +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/raw_ostream.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/Support/LLVM.h" + using namespace mlir; using namespace catalyst::quantum; +#include "Quantum/IR/QuantumInterfaces.cpp.inc" + +//===----------------------------------------------------------------------===// +// Helpers +//===----------------------------------------------------------------------===// + +namespace { +template void printIterable(T iterable, llvm::raw_string_ostream &ss) +{ + ss << "["; + llvm::interleave(iterable, ss, ","); + ss << "]"; +} + +void printAttr(mlir::Attribute attr, llvm::raw_string_ostream &ss) +{ + llvm::TypeSwitch(attr) + .Case([&](mlir::DictionaryAttr dict) { + ss << "{"; + for (auto [i, entry] : llvm::enumerate(dict)) { + if (i > 0) { + ss << ","; + } + + ss << entry.getName().str() << ":"; + printAttr(entry.getValue(), ss); + } + ss << "}"; + }) + .Case([&](mlir::ArrayAttr arr) { + ss << "["; + for (auto [i, attr] : llvm::enumerate(arr)) { + if (i > 0) { + ss << ","; + } + printAttr(attr, ss); + } + ss << "]"; + }) + .Case([&](mlir::StringAttr attr) { ss << attr.str(); }) + .Case([&](mlir::IntegerAttr attr) { ss << attr.getInt(); }) + .Case([&](mlir::FloatAttr attr) { ss << attr.getValue(); }) + .Default([&](mlir::Attribute attr) { attr.print(ss); }); +} +} // namespace + //===----------------------------------------------------------------------===// // Quantum interface definitions. //===----------------------------------------------------------------------===// -#include "Quantum/IR/QuantumInterfaces.cpp.inc" +namespace catalyst { +namespace quantum { + +std::string defaultGetGraphOpId(Operation *op) +{ + std::string out; + llvm::raw_string_ostream ss(out); + + DecomposableGate gate = cast(op); + + ss << gate.getPlName(); + printIterable(gate.getDynamicShape(), ss); + printIterable(gate.getWireLens(), ss); + printAttr(gate.getStaticData(), ss); + return out; +} + +} // namespace quantum +} // namespace catalyst diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 3a48bdc10a..bc5ca4a83d 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include "llvm/ADT/StringSet.h" #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/Casting.h" +#include "llvm/Support/raw_ostream.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/IR/Attributes.h" @@ -1165,12 +1167,14 @@ ParseResult OperatorOp::parse(OpAsmParser &parser, OperationState &result) // Quantum op interface methods. //===----------------------------------------------------------------------===// -mlir::TypeRange PauliRotOp::getDynamicShape() { return getAllParams().getTypes(); } - -std::string PauliRotOp::getDecompId() { return "paulirot" + getPauliWord(); } +// PauliRotOp std::string PauliRotOp::getPlName() { return "PauliRot"; } +mlir::TypeRange PauliRotOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::vector PauliRotOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } + mlir::DictionaryAttr PauliRotOp::getStaticData() { mlir::MLIRContext *ctx = getContext(); @@ -1179,4 +1183,37 @@ mlir::DictionaryAttr PauliRotOp::getStaticData() return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); } -std::vector PauliRotOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } +// OperatorOp + +std::string OperatorOp::getPlName() { return getOpName().str(); } + +mlir::TypeRange OperatorOp::getDynamicShape() { return getParams().getTypes(); } + +std::vector OperatorOp::getWireLens() +{ + if (getInQreg()) { + std::vector lens; + // This assumes static lengths! + // If we enable support for dynamic lengths, we need to update this + for (mlir::Type indexTensor : getArrQubitIndices().getType()) { + for (size_t dim : cast(indexTensor).getShape()) { + lens.push_back(size_t(dim)); + } + } + return lens; + } + return {getInQubits().size()}; +} + +std::string OperatorOp::getGraphOpId() +{ + std::string out; + llvm::raw_string_ostream ss(out); + + ss << defaultGetGraphOpId(getOperation()); + if (getUID().has_value()) { + ss << "[" << std::to_string(getUID().value()) << "]"; + } + + return out; +} diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index 5fe9b70d6a..0896dca65b 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/raw_ostream.h" @@ -27,7 +26,6 @@ #include "nanobind/stl/vector.h" // for automatic vector conversion #include "Quantum/IR/QuantumInterfaces.h" -#include "Quantum/IR/QuantumOps.h" #include "PythonDriverUtils.hpp" @@ -73,7 +71,7 @@ std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) { std::cout << "using new python lowering for op with the following data:\n"; std::cout << "op name: " << op.getPlName() << "\n"; - std::cout << "op ID: " << op.getDecompId() << "\n"; + std::cout << "op ID: " << op.getGraphOpId() << "\n"; std::cout << "dynamic data shape:\n"; for (auto type : op.getDynamicShape()) { @@ -81,7 +79,7 @@ std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) } std::cout << "wire lens:\n"; - for (size_t len : op.getWireLens()) { + for (auto len : op.getWireLens()) { std::cout << len << ", "; } std::cout << "\n"; @@ -105,14 +103,14 @@ std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) nb::object wrapperFunction = wrapperModule.attr(functionName); nb::object pythonResult = wrapperFunction( - op.getPlName(), op.getDecompId(), getPyvalFromTypeRange(op.getDynamicShape()), + op.getPlName(), op.getGraphOpId(), getPyvalFromTypeRange(op.getDynamicShape()), op.getWireLens(), getPyvalFromMlirAttribute(op.getStaticData())); return nb::borrow(pythonResult).c_str(); } catch (const nb::python_error &error) { throw QuantumPythonDecompositions::TracingError(moduleName, functionName, - op.getDecompId(), error.what()); + op.getGraphOpId(), error.what()); } catch (const std::exception &error) { throw; diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index 1c93a671d7..2db271bec8 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -368,7 +368,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase Date: Fri, 10 Jul 2026 09:06:30 -0400 Subject: [PATCH 27/78] add interface tests --- mlir/unittests/Quantum/CMakeLists.txt | 1 + .../Quantum/Interfaces/CMakeLists.txt | 11 + .../TestDecomposableGateInterface.cpp | 245 ++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 mlir/unittests/Quantum/CMakeLists.txt create mode 100644 mlir/unittests/Quantum/Interfaces/CMakeLists.txt create mode 100644 mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp diff --git a/mlir/unittests/Quantum/CMakeLists.txt b/mlir/unittests/Quantum/CMakeLists.txt new file mode 100644 index 0000000000..3ac45c1871 --- /dev/null +++ b/mlir/unittests/Quantum/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Interfaces) diff --git a/mlir/unittests/Quantum/Interfaces/CMakeLists.txt b/mlir/unittests/Quantum/Interfaces/CMakeLists.txt new file mode 100644 index 0000000000..b9fd7b294c --- /dev/null +++ b/mlir/unittests/Quantum/Interfaces/CMakeLists.txt @@ -0,0 +1,11 @@ +add_catalyst_unittest(CatalystQuantumInterfaceTests + TestDecomposableGateInterface.cpp +) + +get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) + +target_link_libraries(CatalystQuantumInterfaceTests PRIVATE + ${dialect_libs} + MLIRQuantum + MLIRQRef +) diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp new file mode 100644 index 0000000000..ea69b591c9 --- /dev/null +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -0,0 +1,245 @@ + +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include "gtest/gtest.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Format.h" // for gtest printing on failure +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/AsmState.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OwningOpRef.h" +#include "mlir/IR/TypeRange.h" +#include "mlir/IR/Types.h" +#include "mlir/Parser/Parser.h" + +#include "Quantum/IR/QuantumDialect.h" +#include "Quantum/IR/QuantumInterfaces.h" +#include "Quantum/IR/QuantumOps.h" + +using namespace mlir; +using namespace catalyst::quantum; + +TEST(DecomposableGateInterfaceTests, PauliRotOp) +{ + std::string moduleStr = R"mlir( +module { + %angle = arith.constant 3.1 : f64 + %q0 = quantum.alloc_qb : !quantum.bit + %q1 = quantum.alloc_qb : !quantum.bit + %q2 = quantum.alloc_qb : !quantum.bit + %0:3 = quantum.paulirot ["X", "Y", "Z"] (%angle) %q0, %q1, %q2 : !quantum.bit, !quantum.bit, !quantum.bit +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + auto paulirots = module->getOps(); + DecomposableGate paulirot = *paulirots.begin(); + + ASSERT_EQ(paulirot.getPlName(), "PauliRot"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::Float64Type::get(&context)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(paulirot.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(paulirot.getWireLens(), std::vector({3})); + + mlir::NamedAttribute entry(mlir::StringAttr::get(&context, "pauli_word"), + mlir::StringAttr::get(&context, "XYZ")); + mlir::DictionaryAttr expectedStaticData = mlir::DictionaryAttr::get(&context, {entry}); + ASSERT_EQ(paulirot.getStaticData(), expectedStaticData); + + ASSERT_EQ(paulirot.getGraphOpId(), "PauliRot[f64][3]{pauli_word:XYZ}"); +} + +TEST(DecomposableGateInterfaceTests, OperatorOpQubits) +{ + std::string moduleStr = R"mlir( +module { + %angle = arith.constant 3.1 : f64 + %flag = arith.constant 0 : i1 + %index = arith.constant 5 : i64 + %q0 = quantum.alloc_qb : !quantum.bit + %q1 = quantum.alloc_qb : !quantum.bit + %0:2 = quantum.operator "testInterfaceOp"(%flag: i1, %angle: f64, %index: i64) qubits(%q0, %q1) static_data = {"myStaticArray"=[1,2,3], "myStaticString"="Test", "myStaticInt"=4} +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + auto operators = module->getOps(); + DecomposableGate op = *operators.begin(); + + ASSERT_EQ(op.getPlName(), "testInterfaceOp"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::IntegerType::get(&context, 1), + mlir::Float64Type::get(&context), + mlir::IntegerType::get(&context, 64)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(op.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(op.getWireLens(), std::vector({2})); + + IntegerType i64 = IntegerType::get(&context, 64); + llvm::SmallVector arr({ + mlir::IntegerAttr::get(i64, 1), + mlir::IntegerAttr::get(i64, 2), + mlir::IntegerAttr::get(i64, 3), + }); + mlir::NamedAttribute arrAttr(mlir::StringAttr::get(&context, "myStaticArray"), + mlir::ArrayAttr::get(&context, arr)); + + mlir::NamedAttribute stringAttr(mlir::StringAttr::get(&context, "myStaticString"), + mlir::StringAttr::get(&context, "Test")); + + mlir::NamedAttribute intAttr(mlir::StringAttr::get(&context, "myStaticInt"), + mlir::IntegerAttr::get(i64, 4)); + mlir::DictionaryAttr expectedStaticData = + mlir::DictionaryAttr::get(&context, {arrAttr, stringAttr, intAttr}); + ASSERT_EQ(op.getStaticData(), expectedStaticData); + + ASSERT_EQ( + op.getGraphOpId(), + "testInterfaceOp[i1,f64,i64][2]{myStaticArray:[1,2,3],myStaticInt:4,myStaticString:Test}"); +} + +TEST(DecomposableGateInterfaceTests, OperatorOpQureg) +{ + std::string moduleStr = R"mlir( +func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { + %angle = arith.constant 3.1 : f64 + %flag = arith.constant 0 : i1 + %index = arith.constant 5 : i64 + + %reg = quantum.alloc(4) : !quantum.reg + + %0 = quantum.operator "testOperatorQreg"(%flag: i1, %angle: f64, %index: i64) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) static_data={"myStaticArray"=[4,2,4], "myStaticString"="string", "myStaticInt"=8} + return +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate op; + module->walk([&](OperatorOp walkOp) { op = walkOp; }); + + ASSERT_EQ(op.getPlName(), "testOperatorQreg"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::IntegerType::get(&context, 1), + mlir::Float64Type::get(&context), + mlir::IntegerType::get(&context, 64)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(op.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(op.getWireLens(), std::vector({1, 2})); + + IntegerType i64 = IntegerType::get(&context, 64); + Float32Type f32 = mlir::Float32Type::get(&context); + llvm::SmallVector arr({ + mlir::IntegerAttr::get(i64, 4), + mlir::FloatAttr::get(f32, 2.4), + mlir::IntegerAttr::get(i64, 4), + }); + mlir::NamedAttribute arrAttr(mlir::StringAttr::get(&context, "myStaticArray"), + mlir::ArrayAttr::get(&context, arr)); + + mlir::NamedAttribute stringAttr(mlir::StringAttr::get(&context, "myStaticString"), + mlir::StringAttr::get(&context, "string")); + + mlir::NamedAttribute intAttr(mlir::StringAttr::get(&context, "myStaticInt"), + mlir::IntegerAttr::get(i64, 8)); + mlir::DictionaryAttr expectedStaticData = + mlir::DictionaryAttr::get(&context, {arrAttr, stringAttr, intAttr}); + ASSERT_EQ(op.getStaticData(), expectedStaticData); + + ASSERT_EQ(op.getGraphOpId(), "testOperatorQreg[i1,f64,i64][1,2]{myStaticArray:[4,2.4,4]," + "myStaticInt:8,myStaticString:string}"); +} + +TEST(DecomposableGateInterfaceTests, OperatorOpUID) +{ + std::string moduleStr = R"mlir( +func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { + %angle = arith.constant 3.1 : f64 + %flag = arith.constant 0 : i1 + %index = arith.constant 5 : i64 + + %reg = quantum.alloc(4) : !quantum.reg + %q0 = quantum.extract %reg[0] : !quantum.reg -> !quantum.bit + + %0 = quantum.operator "testOperatorUID"(%flag: i1, %angle: f64, %index: i64) + UID(248) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) + return +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate op; + module->walk([&](OperatorOp walkOp) { op = walkOp; }); + + ASSERT_EQ(op.getPlName(), "testOperatorUID"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::IntegerType::get(&context, 1), + mlir::Float64Type::get(&context), + mlir::IntegerType::get(&context, 64)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(op.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(op.getWireLens(), std::vector({1, 2})); + + ASSERT_EQ(op.getStaticData(), mlir::DictionaryAttr::get(&context, {})); + + ASSERT_EQ(op.getGraphOpId(), "testOperatorUID[i1,f64,i64][1,2]{}[248]"); +} From a5e03dadd1f02f97f2990441b68bc799a127a83f Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 09:52:35 -0400 Subject: [PATCH 28/78] requested changes --- mlir/lib/Quantum/Transforms/graph_decomposition.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index 0578b99b8a..3636573996 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -140,7 +140,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase moduleOp = + mlir::OwningOpRef builtinModule = mlir::parseSourceFile(filename, config); SymbolTable symbolTable(module); - if (!moduleOp) { + if (!builtinModule) { llvm::errs() << "failed to load built-in decomposition rules from '" << filename << "': the rules file could not be parsed\n"; return failure(); } - for (auto rule : llvm::make_early_inc_range(moduleOp.get().getOps())) { + for (auto rule : + llvm::make_early_inc_range(builtinModule.get().getOps())) { if (failed(addRuleNode(rule, ruleNodes))) { return failure(); } From d9f3df6a17fd07f4cd5d3a2870e0dca0e9b2e09b Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 10:12:07 -0400 Subject: [PATCH 29/78] fix floatattr stringify --- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index dfd57f9769..e1cdbbaf44 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -67,7 +67,7 @@ void printAttr(mlir::Attribute attr, llvm::raw_string_ostream &ss) }) .Case([&](mlir::StringAttr attr) { ss << attr.str(); }) .Case([&](mlir::IntegerAttr attr) { ss << attr.getInt(); }) - .Case([&](mlir::FloatAttr attr) { ss << attr.getValue(); }) + .Case([&](mlir::FloatAttr attr) { ss << attr.getValueAsDouble(); }) .Default([&](mlir::Attribute attr) { attr.print(ss); }); } } // namespace From e3e67367fbaa065c38d6434ce36279f5d8468726 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 10:22:43 -0400 Subject: [PATCH 30/78] improve interface note in graph-decomposition --- mlir/lib/Quantum/Transforms/graph_decomposition.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index aa06948158..afa47301c8 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -430,6 +430,11 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase &operators) { + // TODO: replace this with DecomposableGate interface. We will drop support for any other op + // types once the interface has been implemented for the core operations in the quantum + // dialect. + // The interface will provide one unified way of generating operator nodes from operations, + // with consistent getter methods for all relevant data fields. getOperation().walk([&](quantum::QuantumGate op) { if (isInDecompRule(op)) { return; @@ -443,7 +448,6 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasegetName().stripDialect().str(); if (name == "gphase") { name = "GlobalPhase"; From 0437262f2ecfd8fe19785ed6a9e159ad7dd7cc41 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 10:28:42 -0400 Subject: [PATCH 31/78] improve frontend TODOs --- frontend/catalyst/device/python_decompositions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 68f29ea980..6a2a30e547 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -59,7 +59,8 @@ def rule_to_subroutine(rule): def decomp_rule(*params, wires): rule._impl(*params, *wires, **static_data) - # TODO remove this once we have unified lowering, we should be able to set target_gate + # TODO remove this once we have unified lowering, we should be able to set target_gate and + # stop relying on function names decomp_rule.__name__ = op_id + "_" + rule.name return qp.capture.subroutine(decomp_rule) @@ -76,6 +77,9 @@ def decomp_rule(*params, wires): @qp.qnode(device=device) def circuit(): for subroutine in subroutines: + # TODO I know this is dynamic, but we should probably have a better way of handling this + # than hard-coded dummy values. Revisit this when unifying the decomp-rule lowering + # pipeline subroutine(*[0.5 for _ in dynamic_shape], wires=wires) return str(circuit.mlir_module) From 8ef0e38d938b8e2613cfb5618da8e202034eead1 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 10:34:34 -0400 Subject: [PATCH 32/78] use interface for paulirot lowering --- mlir/lib/Quantum/Transforms/graph_decomposition.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp index afa47301c8..e5d8c27940 100644 --- a/mlir/lib/Quantum/Transforms/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/graph_decomposition.cpp @@ -453,7 +453,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(op.getOperation()).getPauliWord(); + name = cast(op.getOperation()).getGraphOpId(); } node.name = name; } From 133e8fa4713934c3ee7111941feb5a8416e91079 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 10:35:04 -0400 Subject: [PATCH 33/78] remove debug prints --- .../catalyst/device/python_decompositions.py | 8 ------- .../PythonFunction.cpp | 24 ------------------- 2 files changed, 32 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 6a2a30e547..d58f8e206f 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -44,14 +44,6 @@ def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, static_data) -> str: """Generic decomposition wrapper.""" - print( - "Hello from python! Here's the received data:", - op_name, - dynamic_shape, - wire_lens, - static_data, - ) - device = qp.device("null.qubit", wires=sum(wire_lens)) wires = tuple(jnp.array(range(length)) for length in wire_lens) diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index 0896dca65b..db0720efe7 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -69,30 +69,6 @@ nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) { - std::cout << "using new python lowering for op with the following data:\n"; - std::cout << "op name: " << op.getPlName() << "\n"; - std::cout << "op ID: " << op.getGraphOpId() << "\n"; - - std::cout << "dynamic data shape:\n"; - for (auto type : op.getDynamicShape()) { - type.dump(); - } - - std::cout << "wire lens:\n"; - for (auto len : op.getWireLens()) { - std::cout << len << ", "; - } - std::cout << "\n"; - - std::cout << "static data:\n"; - for (auto namedAttr : op.getStaticData()) { - std::cout << namedAttr.getName().getValue().str(); - namedAttr.getValue().dump(); - std::cout << "\n"; - } - - std::cout << "trying nanobind now\n"; - QuantumPythonDecompositions::PyInterpreterGuard guard; std::string mlirText = guard.withGil([&] -> std::string { const char *moduleName = "catalyst.device.python_decompositions"; From 7676c35e991b6e0f5786a4952c067fda9021f075 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 11:01:19 -0400 Subject: [PATCH 34/78] update pytests --- frontend/test/pytest/test_QPD.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/test/pytest/test_QPD.py b/frontend/test/pytest/test_QPD.py index 10683d634b..51d78ec034 100644 --- a/frontend/test/pytest/test_QPD.py +++ b/frontend/test/pytest/test_QPD.py @@ -26,10 +26,10 @@ class TestQPD: def test_paulirot_wrapper(self): """Test that the paulirot QPD wrapper correctly returns the IR as a string.""" result = python_decomposition_wrapper( - "PauliRot", "paulirotXZZ", [0.4], {"pauli_word": "XZZ"}, 3 + "PauliRot", "paulirot[f64][3]{pauli_word:XZZ}", [0.4], [3], {"pauli_word": "XZZ"} ) assert isinstance(result, str) - assert "paulirotXZZ__pauli_rot_decomposition" in result + assert "paulirot[f64][3]{pauli_word:XZZ}__pauli_rot_decomposition" in result assert "Hadamard" in result assert "multirz" in result @@ -47,10 +47,10 @@ def test_decomp(angle, wires, pauli_word): qp.add_decomps(qp.PauliRot, test_decomp) result = python_decomposition_wrapper( - "PauliRot", "paulirotXYX", [float], {"pauli_word": "XYX"}, 3 + "PauliRot", "paulirot[f64][3]{pauli_word:XYX}", [float], [3], {"pauli_word": "XYX"} ) - assert "paulirotXYX_test_decomp" in result + assert "paulirot[f64][3]{pauli_word:XYX}_test_decomp" in result assert "test_decomp" in result From 674f9442ac1da6a7371b3363b573c6eeedf8ce31 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 12:00:20 -0400 Subject: [PATCH 35/78] update DL to support new paulirot ID --- mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp | 2 +- mlir/test/Quantum/DecomposeLoweringTest.mlir | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp b/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp index 26cb46075d..906186b254 100644 --- a/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp +++ b/mlir/lib/Quantum/Transforms/DecomposeLoweringPatterns.cpp @@ -266,7 +266,7 @@ struct DLPauliRotOpPattern : public OpRewritePattern { LogicalResult matchAndRewrite(PauliRotOp op, PatternRewriter &rewriter) const override { - std::string gateName = "paulirot" + op.getPauliWord(); + std::string gateName = cast(op.getOperation()).getGraphOpId(); // Only decompose the op if it is not in the target gate set if (targetGateSet.contains("paulirot")) { diff --git a/mlir/test/Quantum/DecomposeLoweringTest.mlir b/mlir/test/Quantum/DecomposeLoweringTest.mlir index 7a6d9d3566..3b2e1d1d80 100644 --- a/mlir/test/Quantum/DecomposeLoweringTest.mlir +++ b/mlir/test/Quantum/DecomposeLoweringTest.mlir @@ -669,7 +669,7 @@ module @test_paulirot { } // CHECK: my_paulirot_decomp - func.func private @my_paulirot_decomp(%inreg : !quantum.reg, %angle_tensor : tensor, %q_tensor : tensor<3xi64>) -> !quantum.reg attributes {target_gate = "paulirotZXY"} { + func.func private @my_paulirot_decomp(%inreg : !quantum.reg, %angle_tensor : tensor, %q_tensor : tensor<3xi64>) -> !quantum.reg attributes {target_gate = "PauliRot[f64][3]{pauli_word:ZXY}"} { %pi_by_2 = arith.constant 1.57 : f64 %m_pi_by_2 = arith.constant -1.57 : f64 %angle = tensor.extract %angle_tensor[] : tensor From d22d83636aa13c62727c16bc6627f10025829b5e Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 14:38:55 -0400 Subject: [PATCH 36/78] add getExtraData to interface --- mlir/include/Quantum/IR/QuantumInterfaces.td | 5 +++++ mlir/include/Quantum/IR/QuantumOps.td | 2 +- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 1 + mlir/lib/Quantum/IR/QuantumOps.cpp | 12 ++---------- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index ca91f975ff..06068367a8 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -202,6 +202,11 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { "Return the values of static data.", "mlir::DictionaryAttr", "getStaticData" >, + InterfaceMethod< + "Return a string representation of any additional data an operator may need to provide" + " for uniqueness (ex. UID).", + "std::string", "getExtraData", (ins), [{}], [{ return ""; }] + >, InterfaceMethod< "Return the operation's ID for use with the graph. " "This should be computed by the following convention:\n" diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 8cc188cd85..ba2170b3ea 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -536,7 +536,7 @@ def CustomOp : UnitaryGate_Op<"custom", [DifferentiableGate, NoMemoryEffect, def OperatorOp : UnitaryGate_Op<"operator", [ParametrizedGate, NoMemoryEffect, AttrSizedOperandSegments, AttrSizedResultSegments, - DeclareOpInterfaceMethods]> { + DeclareOpInterfaceMethods]> { let summary = "A generalized quantum operation with arbitrary input signature."; let description = [{ The `quantum.operator` is a generic representation for arbitrary quantum operations diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index e1cdbbaf44..3193c56dbf 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -90,6 +90,7 @@ std::string defaultGetGraphOpId(Operation *op) printIterable(gate.getDynamicShape(), ss); printIterable(gate.getWireLens(), ss); printAttr(gate.getStaticData(), ss); + ss << gate.getExtraData(); return out; } diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index bf95400dbe..179a05e93e 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1206,15 +1206,7 @@ std::vector OperatorOp::getWireLens() return {getInQubits().size()}; } -std::string OperatorOp::getGraphOpId() +std::string OperatorOp::getExtraData() { - std::string out; - llvm::raw_string_ostream ss(out); - - ss << defaultGetGraphOpId(getOperation()); - if (getUID().has_value()) { - ss << "[" << std::to_string(getUID().value()) << "]"; - } - - return out; + return getUID().has_value() ? std::to_string(getUID().value()) : ""; } From 00d0207a6c9a7ec34f7a32a783c7ec280de56eec Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 14:57:55 -0400 Subject: [PATCH 37/78] fix casing in QPD test --- frontend/test/pytest/test_QPD.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/test/pytest/test_QPD.py b/frontend/test/pytest/test_QPD.py index 51d78ec034..1d3836ef08 100644 --- a/frontend/test/pytest/test_QPD.py +++ b/frontend/test/pytest/test_QPD.py @@ -26,10 +26,10 @@ class TestQPD: def test_paulirot_wrapper(self): """Test that the paulirot QPD wrapper correctly returns the IR as a string.""" result = python_decomposition_wrapper( - "PauliRot", "paulirot[f64][3]{pauli_word:XZZ}", [0.4], [3], {"pauli_word": "XZZ"} + "PauliRot", "PauliRot[f64][3]{pauli_word:XZZ}", [0.4], [3], {"pauli_word": "XZZ"} ) assert isinstance(result, str) - assert "paulirot[f64][3]{pauli_word:XZZ}__pauli_rot_decomposition" in result + assert "PauliRot[f64][3]{pauli_word:XZZ}__pauli_rot_decomposition" in result assert "Hadamard" in result assert "multirz" in result @@ -47,10 +47,10 @@ def test_decomp(angle, wires, pauli_word): qp.add_decomps(qp.PauliRot, test_decomp) result = python_decomposition_wrapper( - "PauliRot", "paulirot[f64][3]{pauli_word:XYX}", [float], [3], {"pauli_word": "XYX"} + "PauliRot", "PauliRot[f64][3]{pauli_word:XYX}", [float], [3], {"pauli_word": "XYX"} ) - assert "paulirot[f64][3]{pauli_word:XYX}_test_decomp" in result + assert "PauliRot[f64][3]{pauli_word:XYX}_test_decomp" in result assert "test_decomp" in result From 9b33de77d4fa42c3297f510d6600b4f1f0030c40 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 10 Jul 2026 14:59:34 -0400 Subject: [PATCH 38/78] add dtype for wires --- frontend/catalyst/device/python_decompositions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index d58f8e206f..db7d926623 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -45,7 +45,7 @@ def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, static_data) -> str: """Generic decomposition wrapper.""" device = qp.device("null.qubit", wires=sum(wire_lens)) - wires = tuple(jnp.array(range(length)) for length in wire_lens) + wires = tuple(jnp.array(range(length), dtype=int) for length in wire_lens) def rule_to_subroutine(rule): def decomp_rule(*params, wires): From 5570514fe4966691508777b7f2d3ee242b4f06eb Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Mon, 13 Jul 2026 10:29:35 -0400 Subject: [PATCH 39/78] Update mlir/include/Quantum/IR/QuantumInterfaces.td Co-authored-by: David Ittah --- mlir/include/Quantum/IR/QuantumInterfaces.td | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/include/Quantum/IR/QuantumInterfaces.td b/mlir/include/Quantum/IR/QuantumInterfaces.td index 06068367a8..b43e44f493 100644 --- a/mlir/include/Quantum/IR/QuantumInterfaces.td +++ b/mlir/include/Quantum/IR/QuantumInterfaces.td @@ -188,7 +188,7 @@ def DecomposableGate : OpInterface<"DecomposableGate", [QuantumGate]> { let methods = [ InterfaceMethod< - "Return the name of the corresponding PennyLane operator.", "std::string", "getPlName" + "Return the name of the corresponding frontend operator (i.e. the rule supplier).", "std::string", "getOperatorName" >, InterfaceMethod< "Return the shape and dtype of dynamic data.", From fde3c2e4546f94d975e3f5bf4f129aee74db79ec Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Mon, 13 Jul 2026 10:48:53 -0400 Subject: [PATCH 40/78] use getOperatorName --- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 2 +- mlir/lib/Quantum/IR/QuantumOps.cpp | 4 ++-- .../QuantumPythonDecompositions/PythonFunction.cpp | 9 +++++---- .../Quantum/Interfaces/TestDecomposableGateInterface.cpp | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index 3193c56dbf..652b57ad21 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -86,7 +86,7 @@ std::string defaultGetGraphOpId(Operation *op) DecomposableGate gate = cast(op); - ss << gate.getPlName(); + ss << gate.getOperatorName(); printIterable(gate.getDynamicShape(), ss); printIterable(gate.getWireLens(), ss); printAttr(gate.getStaticData(), ss); diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 179a05e93e..d2f906bc73 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1170,7 +1170,7 @@ ParseResult OperatorOp::parse(OpAsmParser &parser, OperationState &result) // PauliRotOp -std::string PauliRotOp::getPlName() { return "PauliRot"; } +std::string PauliRotOp::getOperatorName() { return "PauliRot"; } mlir::TypeRange PauliRotOp::getDynamicShape() { return getAllParams().getTypes(); } @@ -1186,7 +1186,7 @@ mlir::DictionaryAttr PauliRotOp::getStaticData() // OperatorOp -std::string OperatorOp::getPlName() { return getOpName().str(); } +std::string OperatorOp::getOperatorName() { return getOpName().str(); } mlir::TypeRange OperatorOp::getDynamicShape() { return getParams().getTypes(); } diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index db0720efe7..32a21fba12 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -64,7 +64,7 @@ nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) return outTuple; }) .Case([](auto strAttr) { return nb::cast(strAttr.getValue().str()); }) - .Default([](auto attr) { return nb::cast("placeholder"); }); + .Default([](auto attr) { return nb::str("placeholder"); }); } std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) @@ -78,9 +78,10 @@ std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) nb::module_ wrapperModule = nb::module_::import_(moduleName); nb::object wrapperFunction = wrapperModule.attr(functionName); - nb::object pythonResult = wrapperFunction( - op.getPlName(), op.getGraphOpId(), getPyvalFromTypeRange(op.getDynamicShape()), - op.getWireLens(), getPyvalFromMlirAttribute(op.getStaticData())); + nb::object pythonResult = + wrapperFunction(op.getOperatorName(), op.getGraphOpId(), + getPyvalFromTypeRange(op.getDynamicShape()), op.getWireLens(), + getPyvalFromMlirAttribute(op.getStaticData())); return nb::borrow(pythonResult).c_str(); } diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index ea69b591c9..5fc2696bfc 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -64,7 +64,7 @@ module { auto paulirots = module->getOps(); DecomposableGate paulirot = *paulirots.begin(); - ASSERT_EQ(paulirot.getPlName(), "PauliRot"); + ASSERT_EQ(paulirot.getOperatorName(), "PauliRot"); // This is needed to keep the backing array from being deleted llvm::SmallVector backing({mlir::Float64Type::get(&context)}); @@ -105,7 +105,7 @@ module { auto operators = module->getOps(); DecomposableGate op = *operators.begin(); - ASSERT_EQ(op.getPlName(), "testInterfaceOp"); + ASSERT_EQ(op.getOperatorName(), "testInterfaceOp"); // This is needed to keep the backing array from being deleted llvm::SmallVector backing({mlir::IntegerType::get(&context, 1), @@ -165,7 +165,7 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { DecomposableGate op; module->walk([&](OperatorOp walkOp) { op = walkOp; }); - ASSERT_EQ(op.getPlName(), "testOperatorQreg"); + ASSERT_EQ(op.getOperatorName(), "testOperatorQreg"); // This is needed to keep the backing array from being deleted llvm::SmallVector backing({mlir::IntegerType::get(&context, 1), @@ -227,7 +227,7 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { DecomposableGate op; module->walk([&](OperatorOp walkOp) { op = walkOp; }); - ASSERT_EQ(op.getPlName(), "testOperatorUID"); + ASSERT_EQ(op.getOperatorName(), "testOperatorUID"); // This is needed to keep the backing array from being deleted llvm::SmallVector backing({mlir::IntegerType::get(&context, 1), From 3e295d92518921faa65c246120d3a435413beceb Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Mon, 13 Jul 2026 16:47:22 -0400 Subject: [PATCH 41/78] fix unittests --- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 4 +++- mlir/unittests/CMakeLists.txt | 1 + .../Interfaces/TestDecomposableGateInterface.cpp | 11 ++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index 652b57ad21..b40fe50ee1 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -90,7 +90,9 @@ std::string defaultGetGraphOpId(Operation *op) printIterable(gate.getDynamicShape(), ss); printIterable(gate.getWireLens(), ss); printAttr(gate.getStaticData(), ss); - ss << gate.getExtraData(); + if (gate.getExtraData() != "") { + ss << '[' << gate.getExtraData() << ']'; + } return out; } diff --git a/mlir/unittests/CMakeLists.txt b/mlir/unittests/CMakeLists.txt index e2f1dd75d0..2777a92a52 100644 --- a/mlir/unittests/CMakeLists.txt +++ b/mlir/unittests/CMakeLists.txt @@ -12,6 +12,7 @@ endfunction() if (CATALYST_GTEST_AVAILABLE) add_subdirectory(Example) add_subdirectory(QRef) + add_subdirectory(Quantum) add_subdirectory(Utils) endif() diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 5fc2696bfc..27e95736d7 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -150,7 +150,7 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { %reg = quantum.alloc(4) : !quantum.reg - %0 = quantum.operator "testOperatorQreg"(%flag: i1, %angle: f64, %index: i64) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) static_data={"myStaticArray"=[4,2,4], "myStaticString"="string", "myStaticInt"=8} + %0 = quantum.operator "testOperatorQreg"(%flag: i1, %angle: f64, %index: i64) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) static_data={"myStaticArray"=[4,2.4,4], "myStaticString"="string", "myStaticInt"=8} return } )mlir"; @@ -178,10 +178,10 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { ASSERT_EQ(op.getWireLens(), std::vector({1, 2})); IntegerType i64 = IntegerType::get(&context, 64); - Float32Type f32 = mlir::Float32Type::get(&context); + Float64Type f64 = mlir::Float64Type::get(&context); llvm::SmallVector arr({ mlir::IntegerAttr::get(i64, 4), - mlir::FloatAttr::get(f32, 2.4), + mlir::FloatAttr::get(f64, 2.4), mlir::IntegerAttr::get(i64, 4), }); mlir::NamedAttribute arrAttr(mlir::StringAttr::get(&context, "myStaticArray"), @@ -196,8 +196,9 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { mlir::DictionaryAttr::get(&context, {arrAttr, stringAttr, intAttr}); ASSERT_EQ(op.getStaticData(), expectedStaticData); - ASSERT_EQ(op.getGraphOpId(), "testOperatorQreg[i1,f64,i64][1,2]{myStaticArray:[4,2.4,4]," - "myStaticInt:8,myStaticString:string}"); + ASSERT_EQ(op.getGraphOpId(), + "testOperatorQreg[i1,f64,i64][1,2]{myStaticArray:[4,2.400000e+00,4]," + "myStaticInt:8,myStaticString:string}"); } TEST(DecomposableGateInterfaceTests, OperatorOpUID) From 9be1f4f88da9e687835eee6e021ab94a8d08af18 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 11:22:00 -0400 Subject: [PATCH 42/78] Implement decomposable gate interface methods for other gate ops --- mlir/include/Quantum/IR/QuantumOps.td | 3 +- mlir/lib/Quantum/IR/QuantumOps.cpp | 13 ++++++ .../TestDecomposableGateInterface.cpp | 46 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index ba2170b3ea..c8dd90621e 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -778,7 +778,8 @@ def GlobalPhaseOp : UnitaryGate_Op<"gphase", [DifferentiableGate, AttrSizedOpera } def MultiRZOp : UnitaryGate_Op<"multirz", [DifferentiableGate, NoMemoryEffect, - AttrSizedOperandSegments, AttrSizedResultSegments]> { + AttrSizedOperandSegments, AttrSizedResultSegments, + DeclareOpInterfaceMethods]> { let summary = "Apply an arbitrary multi Z rotation"; let description = [{ The `quantum.multirz` operation applies an arbitrary multi Z rotation to the state-vector. diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index d2f906bc73..7e4f284677 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1168,6 +1168,19 @@ ParseResult OperatorOp::parse(OpAsmParser &parser, OperationState &result) // Quantum op interface methods. //===----------------------------------------------------------------------===// +// MultiRZOp + +std::string MultiRZOp::getOperatorName() { return "MultiRZ"; } + +mlir::TypeRange MultiRZOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::vector MultiRZOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } + +mlir::DictionaryAttr MultiRZOp::getStaticData() +{ + return mlir::DictionaryAttr::get(getContext(), {}); +} + // PauliRotOp std::string PauliRotOp::getOperatorName() { return "PauliRot"; } diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 27e95736d7..2e23377669 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -42,6 +42,42 @@ using namespace mlir; using namespace catalyst::quantum; +TEST(DecomposableGateInterfaceTests, MultiRZOp) +{ + std::string moduleStr = R"mlir( +module { + %angle = arith.constant 3.1 : f64 + %q0 = quantum.alloc_qb : !quantum.bit + %q1 = quantum.alloc_qb : !quantum.bit + %q2 = quantum.alloc_qb : !quantum.bit + %mrz:3 = quantum.multirz(%angle) %q0, %q1, %q2 : !quantum.bit, !quantum.bit, !quantum.bit +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate multiRZ = *module->getOps().begin(); + + ASSERT_EQ(multiRZ.getOperatorName(), "MultiRZ"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::Float64Type::get(&context)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(multiRZ.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(multiRZ.getWireLens(), std::vector({3})); + + ASSERT_EQ(multiRZ.getStaticData().size(), 0); + + ASSERT_EQ(multiRZ.getGraphOpId(), "MultiRZ[f64][3]{}"); +} + TEST(DecomposableGateInterfaceTests, PauliRotOp) { std::string moduleStr = R"mlir( @@ -147,7 +183,7 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { %angle = arith.constant 3.1 : f64 %flag = arith.constant 0 : i1 %index = arith.constant 5 : i64 - + %reg = quantum.alloc(4) : !quantum.reg %0 = quantum.operator "testOperatorQreg"(%flag: i1, %angle: f64, %index: i64) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) static_data={"myStaticArray"=[4,2.4,4], "myStaticString"="string", "myStaticInt"=8} @@ -208,12 +244,12 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { %angle = arith.constant 3.1 : f64 %flag = arith.constant 0 : i1 %index = arith.constant 5 : i64 - + %reg = quantum.alloc(4) : !quantum.reg %q0 = quantum.extract %reg[0] : !quantum.reg -> !quantum.bit - - %0 = quantum.operator "testOperatorUID"(%flag: i1, %angle: f64, %index: i64) - UID(248) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) + + %0 = quantum.operator "testOperatorUID"(%flag: i1, %angle: f64, %index: i64) + UID(248) quregs(%reg) indices(%first: tensor<1xi64>, %secondthird: tensor<2xi64>) return } )mlir"; From 77ccac8a7513af511e174fd4bbc366d39669bc7b Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 11:42:28 -0400 Subject: [PATCH 43/78] requested changes --- frontend/test/pytest/test_QPD.py | 2 +- .../QuantumPythonDecompositions/PythonFunction.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/test/pytest/test_QPD.py b/frontend/test/pytest/test_QPD.py index 1d3836ef08..a4986d9d7d 100644 --- a/frontend/test/pytest/test_QPD.py +++ b/frontend/test/pytest/test_QPD.py @@ -41,7 +41,7 @@ def test_resources(): return {qp.X: 1} @qp.register_resources(test_resources) - def test_decomp(angle, wires, pauli_word): + def test_decomp(angle, wires, pauli_word): # pylint: disable=unused-argument qp.RX(angle, wires[0]) qp.add_decomps(qp.PauliRot, test_decomp) diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index 32a21fba12..c00e810207 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -13,7 +13,6 @@ // limitations under the License. #include -#include #include #include "llvm/ADT/TypeSwitch.h" @@ -33,7 +32,9 @@ namespace nb = nanobind; -nb::object getPyvalFromTypeRange(mlir::TypeRange typerange) +namespace { + +static nb::object getPyvalFromTypeRange(mlir::TypeRange typerange) { nb::list pyTypes; for (auto type : typerange) { @@ -45,7 +46,7 @@ nb::object getPyvalFromTypeRange(mlir::TypeRange typerange) return pyTypes; } -nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) +static nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) { return llvm::TypeSwitch(attr) .Case([](auto dictAttr) { @@ -67,6 +68,8 @@ nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) .Default([](auto attr) { return nb::str("placeholder"); }); } +} // namespace + std::string pythonRuleLowering(catalyst::quantum::DecomposableGate op) { QuantumPythonDecompositions::PyInterpreterGuard guard; From 58346be4ef95772a60487993ac59bdb40a34e765 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 11:57:57 -0400 Subject: [PATCH 44/78] pcphase --- mlir/include/Quantum/IR/QuantumOps.td | 7 ++-- mlir/lib/Quantum/IR/QuantumOps.cpp | 13 ++++++ .../GraphDecomposition/TestFailedDecomp.mlir | 8 ++-- .../TestDecomposableGateInterface.cpp | 42 ++++++++++++++++++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index c8dd90621e..21d4bfd905 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -818,7 +818,8 @@ def MultiRZOp : UnitaryGate_Op<"multirz", [DifferentiableGate, NoMemoryEffect, } def PCPhaseOp : UnitaryGate_Op<"pcphase", [DifferentiableGate, NoMemoryEffect, - AttrSizedOperandSegments, AttrSizedResultSegments]> { + AttrSizedOperandSegments, AttrSizedResultSegments, + DeclareOpInterfaceMethods]> { let summary = "Apply a projector-controlled phase gate"; let description = [{ This gate is built from simpler gates like `PhaseShift` and `PauliX` and acts on a group @@ -858,8 +859,8 @@ def PCPhaseOp : UnitaryGate_Op<"pcphase", [DifferentiableGate, NoMemoryEffect, }]; let extraClassDeclaration = extraBaseClassDeclaration # [{ - mlir::ValueRange getAllParams() { - return getODSOperands(getParamOperandIdx()); + mlir::OperandRange getAllParams() { + return getOperands().slice(getParamOperandIdx(), getParamOperandIdx()+2); } }]; diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 7e4f284677..570a9fabd3 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1197,6 +1197,19 @@ mlir::DictionaryAttr PauliRotOp::getStaticData() return mlir::DictionaryAttr::get(ctx, {pauliWordEntry}); } +// PCPhaseOp + +std::string PCPhaseOp::getOperatorName() { return "PCPhase"; } + +mlir::TypeRange PCPhaseOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::vector PCPhaseOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } + +mlir::DictionaryAttr PCPhaseOp::getStaticData() +{ + return mlir::DictionaryAttr::get(getContext(), {}); +} + // OperatorOp std::string OperatorOp::getOperatorName() { return getOpName().str(); } diff --git a/mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir b/mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir index 82ff3cde2b..a7586cd434 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir +++ b/mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir @@ -15,10 +15,10 @@ // RUN: not --crash quantum-opt --split-input-file --pass-pipeline='builtin.module( graph-decomposition{gate-set=PauliX=1.0 bytecode-rules="%BYTECODE_PATH"})' %s 2>&1 | FileCheck %s func.func @circuit(%q0: !quantum.bit) { - %pi = arith.constant 3.14 : f64 - %dim = arith.constant 3.0 : f64 - %out = quantum.pcphase (%pi, %dim) %q0 : !quantum.bit + // Gateset only has X, can never make a Hadamard + %out = quantum.custom "Hadamard"() %q0 : !quantum.bit + // CHECK: GraphSolverFailedError - // CHECK: Decomposition rule not found for operator 'pcphase + // CHECK: Decomposition rule not found for operator 'Hadamard return } diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 2e23377669..8f1f9c0c5f 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -97,8 +97,7 @@ module { ParserConfig config(&context, /*verifyAfterParse=*/false); OwningOpRef module = parseSourceString(moduleStr, config); - auto paulirots = module->getOps(); - DecomposableGate paulirot = *paulirots.begin(); + DecomposableGate paulirot = *module->getOps().begin(); ASSERT_EQ(paulirot.getOperatorName(), "PauliRot"); @@ -118,6 +117,45 @@ module { ASSERT_EQ(paulirot.getGraphOpId(), "PauliRot[f64][3]{pauli_word:XYZ}"); } +TEST(DecomposableGateInterfaceTests, PCPhaseOP) +{ + std::string moduleStr = R"mlir( +module { + %theta = arith.constant 3.7 : f64 + %dim = arith.constant 42.0 : f64 + %q0 = quantum.alloc_qb : !quantum.bit + %q1 = quantum.alloc_qb : !quantum.bit + %q2 = quantum.alloc_qb : !quantum.bit + %oq0, %oq1, %oq2 = quantum.pcphase(%theta, %dim) %q0, %q1 ctrls(%q2) : !quantum.bit, !quantum.bit ctrls !quantum.bit +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate pcphase = *module->getOps().begin(); + + ASSERT_EQ(pcphase.getOperatorName(), "PCPhase"); + + // This is needed to keep the backing array from being deleted + Type f64Type = mlir::Float64Type::get(&context); + llvm::SmallVector backing({f64Type, f64Type}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(pcphase.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + // Controls are not part of the gate wires considered by the decomp interface + ASSERT_EQ(pcphase.getWireLens(), std::vector({2})); + + ASSERT_EQ(pcphase.getStaticData().size(), 0); + + ASSERT_EQ(pcphase.getGraphOpId(), "PCPhase[f64,f64][2]{}"); +} + TEST(DecomposableGateInterfaceTests, OperatorOpQubits) { std::string moduleStr = R"mlir( From 18b79712e0a030cd3ef599f76b5fcbca79180121 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 13:58:43 -0400 Subject: [PATCH 45/78] use op_name for list_decomps --- frontend/catalyst/device/python_decompositions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index db7d926623..3edec55384 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -58,9 +58,7 @@ def decomp_rule(*params, wires): return qp.capture.subroutine(decomp_rule) # let this fail with the standard error message if the op is not found - op_class = getattr(qp, op_name) - - subroutines = [rule_to_subroutine(rule) for rule in qp.decomposition.list_decomps(op_class)] + subroutines = [rule_to_subroutine(rule) for rule in qp.decomposition.list_decomps(op_name)] @qp.qjit( target="mlir", From c94e974848758e6ba734f87ca58cc037446eb593 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 15:15:32 -0400 Subject: [PATCH 46/78] customop --- mlir/include/Quantum/IR/QuantumOps.td | 3 +- mlir/lib/Quantum/IR/QuantumOps.cpp | 13 +++++++ .../TestDecomposableGateInterface.cpp | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 21d4bfd905..7f83050955 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -410,7 +410,8 @@ def SetBasisStateOp : Gate_Op<"set_basis_state"> { } def CustomOp : UnitaryGate_Op<"custom", [DifferentiableGate, NoMemoryEffect, - AttrSizedOperandSegments, AttrSizedResultSegments]> { + AttrSizedOperandSegments, AttrSizedResultSegments, + DeclareOpInterfaceMethods]> { let summary = "A generic quantum gate on n qubits with m floating point parameters."; let description = [{ }]; diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 570a9fabd3..17d223f1b7 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1168,6 +1168,19 @@ ParseResult OperatorOp::parse(OpAsmParser &parser, OperationState &result) // Quantum op interface methods. //===----------------------------------------------------------------------===// +// CustomOp + +std::string CustomOp::getOperatorName() { return getGateName().str(); } + +mlir::TypeRange CustomOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::vector CustomOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } + +mlir::DictionaryAttr CustomOp::getStaticData() +{ + return mlir::DictionaryAttr::get(getContext(), {}); +} + // MultiRZOp std::string MultiRZOp::getOperatorName() { return "MultiRZ"; } diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 8f1f9c0c5f..b81b3d231f 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -42,6 +42,41 @@ using namespace mlir; using namespace catalyst::quantum; +TEST(DecomposableGateInterfaceTests, CustomOp) +{ + std::string moduleStr = R"mlir( +module { + %angle = arith.constant 3.1 : f64 + %q0 = quantum.alloc_qb : !quantum.bit + %q1 = quantum.alloc_qb : !quantum.bit + %oq0, %oq1 = quantum.custom "RX"(%angle) %q0, %q1 : !quantum.bit, !quantum.bit +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate customOp = *module->getOps().begin(); + + ASSERT_EQ(customOp.getOperatorName(), "RX"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::Float64Type::get(&context)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(customOp.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(customOp.getWireLens(), std::vector({2})); + + ASSERT_EQ(customOp.getStaticData().size(), 0); + + ASSERT_EQ(customOp.getGraphOpId(), "RX[f64][2]{}"); +} + TEST(DecomposableGateInterfaceTests, MultiRZOp) { std::string moduleStr = R"mlir( From 7e7cd1a829bf11b48392f338f289cf12ef244026 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 15:29:04 -0400 Subject: [PATCH 47/78] move tests to frontend and use `catalyst --tool=opt` instead of `quantum-opt` --- .../test/lit}/GraphDecomposition/TestAltDecomps.mlir | 6 +++--- .../test/lit}/GraphDecomposition/TestCost.mlir | 2 +- .../test/lit}/GraphDecomposition/TestFailedDecomp.mlir | 2 +- .../test/lit}/GraphDecomposition/TestFixedDecomp.mlir | 2 +- .../test/lit}/GraphDecomposition/TestGatesetRXRZ.mlir | 2 +- .../test/lit}/GraphDecomposition/TestGatesetRYRZ.mlir | 2 +- .../test/lit}/GraphDecomposition/TestIdentity.mlir | 2 +- .../test/lit}/GraphDecomposition/TestMultiDecomp.mlir | 4 ++-- .../lit}/GraphDecomposition/TestMultiDecompUser.mlir | 2 +- .../test/lit}/GraphDecomposition/test_rules.mlir | 0 mlir/test/CMakeLists.txt | 10 +--------- 11 files changed, 13 insertions(+), 21 deletions(-) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestAltDecomps.mlir (76%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestCost.mlir (84%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestFailedDecomp.mlir (82%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestFixedDecomp.mlir (85%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestGatesetRXRZ.mlir (85%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestGatesetRYRZ.mlir (85%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestIdentity.mlir (90%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestMultiDecomp.mlir (65%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/TestMultiDecompUser.mlir (74%) rename {mlir/test/Quantum => frontend/test/lit}/GraphDecomposition/test_rules.mlir (100%) diff --git a/mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir b/frontend/test/lit/GraphDecomposition/TestAltDecomps.mlir similarity index 76% rename from mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir rename to frontend/test/lit/GraphDecomposition/TestAltDecomps.mlir index 8c2d5d2e62..bf0323a4f9 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestAltDecomps.mlir +++ b/frontend/test/lit/GraphDecomposition/TestAltDecomps.mlir @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: quantum-opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RY=1.0,PauliX=3.0,PauliZ=3.0,GlobalPhase=1.0 alt-decomps=PauliY=[y_to_ry,y_to_x_z] bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes RY +// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RY=1.0,PauliX=3.0,PauliZ=3.0,GlobalPhase=1.0 alt-decomps=PauliY=[y_to_ry,y_to_x_z] bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes RY -// RUN: quantum-opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RY=3.0,PauliX=1.0,PauliZ=1.0,GlobalPhase=1.0 alt-decomps=PauliY=[y_to_ry,y_to_x_z] bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes XZ +// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RY=3.0,PauliX=1.0,PauliZ=1.0,GlobalPhase=1.0 alt-decomps=PauliY=[y_to_ry,y_to_x_z] bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes XZ func.func @circuit() -> !quantum.bit { %0 = quantum.alloc(2) : !quantum.reg @@ -22,7 +22,7 @@ func.func @circuit() -> !quantum.bit { // RY-NOT: PauliY // RY: RY // RY: gphase - + // XZ-NOT: PauliY // XZ: PauliX // XZ: PauliZ diff --git a/mlir/test/Quantum/GraphDecomposition/TestCost.mlir b/frontend/test/lit/GraphDecomposition/TestCost.mlir similarity index 84% rename from mlir/test/Quantum/GraphDecomposition/TestCost.mlir rename to frontend/test/lit/GraphDecomposition/TestCost.mlir index a2c661f921..e30f567078 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestCost.mlir +++ b/frontend/test/lit/GraphDecomposition/TestCost.mlir @@ -14,7 +14,7 @@ // Test that decomposition chooses cheapest decomposition path -// RUN: quantum-opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,RY=1.0,RZ=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s +// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,RY=1.0,RZ=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s func.func @circuit() -> !quantum.bit { diff --git a/mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir b/frontend/test/lit/GraphDecomposition/TestFailedDecomp.mlir similarity index 82% rename from mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir rename to frontend/test/lit/GraphDecomposition/TestFailedDecomp.mlir index a7586cd434..406a05076a 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestFailedDecomp.mlir +++ b/frontend/test/lit/GraphDecomposition/TestFailedDecomp.mlir @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: not --crash quantum-opt --split-input-file --pass-pipeline='builtin.module( graph-decomposition{gate-set=PauliX=1.0 bytecode-rules="%BYTECODE_PATH"})' %s 2>&1 | FileCheck %s +// RUN: not --crash catalyst --tool=opt --split-input-file --pass-pipeline='builtin.module( graph-decomposition{gate-set=PauliX=1.0 bytecode-rules="%BYTECODE_PATH"})' %s 2>&1 | FileCheck %s func.func @circuit(%q0: !quantum.bit) { // Gateset only has X, can never make a Hadamard diff --git a/mlir/test/Quantum/GraphDecomposition/TestFixedDecomp.mlir b/frontend/test/lit/GraphDecomposition/TestFixedDecomp.mlir similarity index 85% rename from mlir/test/Quantum/GraphDecomposition/TestFixedDecomp.mlir rename to frontend/test/lit/GraphDecomposition/TestFixedDecomp.mlir index 245d404a76..7547150347 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestFixedDecomp.mlir +++ b/frontend/test/lit/GraphDecomposition/TestFixedDecomp.mlir @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: quantum-opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=2.0,RY=1.0,RZ=1.0,GlobalPhase=0.0 fixed-decomps=Hadamard=custom_decomp bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s +// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=2.0,RY=1.0,RZ=1.0,GlobalPhase=0.0 fixed-decomps=Hadamard=custom_decomp bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s func.func @circuit() -> !quantum.bit { %0 = quantum.alloc(1) : !quantum.reg diff --git a/mlir/test/Quantum/GraphDecomposition/TestGatesetRXRZ.mlir b/frontend/test/lit/GraphDecomposition/TestGatesetRXRZ.mlir similarity index 85% rename from mlir/test/Quantum/GraphDecomposition/TestGatesetRXRZ.mlir rename to frontend/test/lit/GraphDecomposition/TestGatesetRXRZ.mlir index 3a993d530a..5b24777ae1 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestGatesetRXRZ.mlir +++ b/frontend/test/lit/GraphDecomposition/TestGatesetRXRZ.mlir @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: quantum-opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,RZ=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s +// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,RZ=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s func.func public @circuit() attributes {quantum.node} { %0 = quantum.alloc( 1) : !quantum.reg diff --git a/mlir/test/Quantum/GraphDecomposition/TestGatesetRYRZ.mlir b/frontend/test/lit/GraphDecomposition/TestGatesetRYRZ.mlir similarity index 85% rename from mlir/test/Quantum/GraphDecomposition/TestGatesetRYRZ.mlir rename to frontend/test/lit/GraphDecomposition/TestGatesetRYRZ.mlir index 39a3481d7c..f5457d14d0 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestGatesetRYRZ.mlir +++ b/frontend/test/lit/GraphDecomposition/TestGatesetRYRZ.mlir @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: quantum-opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RY=1.0,RZ=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s +// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=RY=1.0,RZ=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s func.func public @circuit() attributes {quantum.node} { %0 = quantum.alloc( 1) : !quantum.reg diff --git a/mlir/test/Quantum/GraphDecomposition/TestIdentity.mlir b/frontend/test/lit/GraphDecomposition/TestIdentity.mlir similarity index 90% rename from mlir/test/Quantum/GraphDecomposition/TestIdentity.mlir rename to frontend/test/lit/GraphDecomposition/TestIdentity.mlir index ff61bc45ad..b27672dbef 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestIdentity.mlir +++ b/frontend/test/lit/GraphDecomposition/TestIdentity.mlir @@ -14,7 +14,7 @@ // Test that decomposition handles circuits that are already decomposed to the given gateset. -// RUN: quantum-opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=Hadamard=1.0,CNOT=1.0 alt-decomps=Hadamard=false_decomp bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s +// RUN: catalyst --tool=opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=Hadamard=1.0,CNOT=1.0 alt-decomps=Hadamard=false_decomp bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s func.func @circuit() -> !quantum.bit { %0 = quantum.alloc(2) : !quantum.reg diff --git a/mlir/test/Quantum/GraphDecomposition/TestMultiDecomp.mlir b/frontend/test/lit/GraphDecomposition/TestMultiDecomp.mlir similarity index 65% rename from mlir/test/Quantum/GraphDecomposition/TestMultiDecomp.mlir rename to frontend/test/lit/GraphDecomposition/TestMultiDecomp.mlir index 7694aef945..35256498cb 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestMultiDecomp.mlir +++ b/frontend/test/lit/GraphDecomposition/TestMultiDecomp.mlir @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: quantum-opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes FIRST +// RUN: catalyst --tool=opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes FIRST -// RUN: quantum-opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"},graph-decomposition{gate-set=RZ=1.0,RY=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes SECOND +// RUN: catalyst --tool=opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=RX=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"},graph-decomposition{gate-set=RZ=1.0,RY=1.0,GlobalPhase=1.0 bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes SECOND func.func @circuit() -> !quantum.bit { %0 = quantum.alloc(2) : !quantum.reg diff --git a/mlir/test/Quantum/GraphDecomposition/TestMultiDecompUser.mlir b/frontend/test/lit/GraphDecomposition/TestMultiDecompUser.mlir similarity index 74% rename from mlir/test/Quantum/GraphDecomposition/TestMultiDecompUser.mlir rename to frontend/test/lit/GraphDecomposition/TestMultiDecompUser.mlir index 6e8b75e72d..c7a206cb33 100644 --- a/mlir/test/Quantum/GraphDecomposition/TestMultiDecompUser.mlir +++ b/frontend/test/lit/GraphDecomposition/TestMultiDecompUser.mlir @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// RUN: quantum-opt --split-input-file --pass-pipeline='builtin.module( graph-decomposition{gate-set=Hadamard=1.0 fixed-decomps=PauliX=x_to_h bytecode-rules="%BYTECODE_PATH"}, graph-decomposition{gate-set=PauliX=1.0 fixed-decomps=Hadamard=h_to_x bytecode-rules="%BYTECODE_PATH"}, graph-decomposition{gate-set=Hadamard=1.0 fixed-decomps=PauliX=x_to_h bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes TRIPLE +// RUN: catalyst --tool=opt --split-input-file --pass-pipeline='builtin.module( graph-decomposition{gate-set=Hadamard=1.0 fixed-decomps=PauliX=x_to_h bytecode-rules="%BYTECODE_PATH"}, graph-decomposition{gate-set=PauliX=1.0 fixed-decomps=Hadamard=h_to_x bytecode-rules="%BYTECODE_PATH"}, graph-decomposition{gate-set=Hadamard=1.0 fixed-decomps=PauliX=x_to_h bytecode-rules="%BYTECODE_PATH"})' %s | FileCheck %s --check-prefixes TRIPLE func.func @circuit() -> !quantum.bit { %0 = quantum.alloc(2) : !quantum.reg diff --git a/mlir/test/Quantum/GraphDecomposition/test_rules.mlir b/frontend/test/lit/GraphDecomposition/test_rules.mlir similarity index 100% rename from mlir/test/Quantum/GraphDecomposition/test_rules.mlir rename to frontend/test/lit/GraphDecomposition/test_rules.mlir diff --git a/mlir/test/CMakeLists.txt b/mlir/test/CMakeLists.txt index 8806b8fce6..beff62e79f 100644 --- a/mlir/test/CMakeLists.txt +++ b/mlir/test/CMakeLists.txt @@ -1,5 +1,3 @@ -set(BYTECODE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Quantum/GraphDecomposition/test_rules.mlirbc") - configure_lit_site_cfg( ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py @@ -43,15 +41,9 @@ endif() list(APPEND DIALECT_TESTS_DEPEND CatalystUnitTests) -add_custom_target(create_test_bytecode - quantum-opt --emit-bytecode --register-decomp-rule-resource ${CMAKE_CURRENT_SOURCE_DIR}/Quantum/GraphDecomposition/test_rules.mlir -o ${BYTECODE_PATH} - DEPENDS quantum-opt - BYPRODUCTS ${BYTECODE_PATH} -) - add_lit_testsuite(check-dialects "Run the regression tests for mlir dialects" ${TEST_SUITES} - DEPENDS ${DIALECT_TESTS_DEPEND} create_test_bytecode + DEPENDS ${DIALECT_TESTS_DEPEND} ) set_target_properties(check-dialects PROPERTIES FOLDER "Tests") From 4577db942dbde1810024ddf9a44c23681ec325dc Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 15:41:11 -0400 Subject: [PATCH 48/78] gphase op --- mlir/include/Quantum/IR/QuantumOps.td | 3 +- mlir/lib/Quantum/IR/QuantumOps.cpp | 13 ++++++++ .../TestDecomposableGateInterface.cpp | 33 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 7f83050955..074a10afbf 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -738,7 +738,8 @@ def PauliRotOp : UnitaryGate_Op<"paulirot", [DifferentiableGate, NoMemoryEffect, let hasVerifier = 1; } -def GlobalPhaseOp : UnitaryGate_Op<"gphase", [DifferentiableGate, AttrSizedOperandSegments]> { +def GlobalPhaseOp : UnitaryGate_Op<"gphase", [DifferentiableGate, AttrSizedOperandSegments, + DeclareOpInterfaceMethods]> { let summary = "Global Phase."; let description = [{ diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 17d223f1b7..d4afe621f8 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1223,6 +1223,19 @@ mlir::DictionaryAttr PCPhaseOp::getStaticData() return mlir::DictionaryAttr::get(getContext(), {}); } +// GlobalPhaseOp + +std::string GlobalPhaseOp::getOperatorName() { return "GlobalPhase"; } + +mlir::TypeRange GlobalPhaseOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::vector GlobalPhaseOp::getWireLens() { return {0}; } + +mlir::DictionaryAttr GlobalPhaseOp::getStaticData() +{ + return mlir::DictionaryAttr::get(getContext(), {}); +} + // OperatorOp std::string OperatorOp::getOperatorName() { return getOpName().str(); } diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index b81b3d231f..5b255f617c 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -191,6 +191,39 @@ module { ASSERT_EQ(pcphase.getGraphOpId(), "PCPhase[f64,f64][2]{}"); } +TEST(DecomposableGateInterfaceTests, GlobalPhaseOp) +{ + std::string moduleStr = R"mlir( +module { + %angle = arith.constant 3.1 : f64 + quantum.gphase(%angle) +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate gphase = *module->getOps().begin(); + + ASSERT_EQ(gphase.getOperatorName(), "GlobalPhase"); + + // This is needed to keep the backing array from being deleted + llvm::SmallVector backing({mlir::Float64Type::get(&context)}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(gphase.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(gphase.getWireLens(), std::vector({0})); + + ASSERT_EQ(gphase.getStaticData().size(), 0); + + ASSERT_EQ(gphase.getGraphOpId(), "GlobalPhase[f64][0]{}"); +} + TEST(DecomposableGateInterfaceTests, OperatorOpQubits) { std::string moduleStr = R"mlir( From 644ac3927abad407b95eca62edc56354e15c5c30 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 16:04:25 -0400 Subject: [PATCH 49/78] changelog --- doc/releases/changelog-dev.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 65239da1b1..6f9352d409 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -238,9 +238,14 @@

Internal changes ⚙️

+<<<<<<< Updated upstream * The `cond` PLxPR primitive's lowering rule no longer expects a `True` Literal for the predicate of the default else branch. [(#3018)](https://github.com/PennyLaneAI/catalyst/pull/3018) +======= +* Add the `DecomposableGate` op interface to allow generic handling of operations in the `graph-decomposition` pass. This allows arbitrary operations implementing the interface to be registered to and decomposed by the graph. This also allows the use of python-decompositions for any operator pre-registered in the frontend graph. + [(#2983)](https://github.com/PennyLaneAI/catalyst/pull/2983) +>>>>>>> Stashed changes * The `graph-decomposition` pass eliminates three redundant IR manipulations: the cloning, removal, and re-insertion of user rules. This optimization is particularly From a4d67e60f01bbcff97e7f6af3dbc47dd23afe246 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 16:09:29 -0400 Subject: [PATCH 50/78] frontend lit test find test_rules.mlirbc --- frontend/test/lit/lit.cfg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/test/lit/lit.cfg.py b/frontend/test/lit/lit.cfg.py index 7b79fefebd..350c80b327 100644 --- a/frontend/test/lit/lit.cfg.py +++ b/frontend/test/lit/lit.cfg.py @@ -23,7 +23,7 @@ # Define the file extensions to treat as test files (with the exception of this file). config.suffixes = [".py", ".mlir"] -config.excludes = ["lit.cfg.py", "utils.py", "catalyst.autograph.exclusion.py"] +config.excludes = ["lit.cfg.py", "utils.py", "catalyst.autograph.exclusion.py", "test_rules.mlir"] # Define the root path of where to look for tests. config.test_source_root = os.path.dirname(__file__) @@ -68,6 +68,9 @@ ) config.substitutions.append(("%PYTHON", python_executable)) +config.substitutions.append( + ("%BYTECODE_PATH", config.test_source_root + "/GraphDecomposition/test_rules.mlirbc") +) # allow virtual env for embedded interpreter config.environment["VIRTUAL_ENV"] = os.getenv("VIRTUAL_ENV", "") From 3b419805e39f1eb46288fffcf2e47d9a7217274f Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 14 Jul 2026 16:58:11 -0400 Subject: [PATCH 51/78] try except on bad rules --- .../catalyst/device/python_decompositions.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 3edec55384..0fdf0ee697 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -60,16 +60,20 @@ def decomp_rule(*params, wires): # let this fail with the standard error message if the op is not found subroutines = [rule_to_subroutine(rule) for rule in qp.decomposition.list_decomps(op_name)] - @qp.qjit( - target="mlir", - capture=True, - ) - @qp.qnode(device=device) - def circuit(): - for subroutine in subroutines: - # TODO I know this is dynamic, but we should probably have a better way of handling this - # than hard-coded dummy values. Revisit this when unifying the decomp-rule lowering - # pipeline - subroutine(*[0.5 for _ in dynamic_shape], wires=wires) - - return str(circuit.mlir_module) + try: + + @qp.qjit( + target="mlir", + capture=True, + ) + @qp.qnode(device=device) + def circuit(): + for subroutine in subroutines: + # TODO I know this is dynamic, but we should probably have a better way of handling this + # than hard-coded dummy values. Revisit this when unifying the decomp-rule lowering + # pipeline + subroutine(*[0.5 for _ in dynamic_shape], wires=wires) + + return str(circuit.mlir_module) + except: + return "builtin.module{}" From d8462be4a98a696d826bc8a7d44f9ef7dffc17f2 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Wed, 15 Jul 2026 10:40:20 -0400 Subject: [PATCH 52/78] unitary op --- mlir/include/Quantum/IR/QuantumOps.td | 3 +- mlir/lib/Quantum/IR/QuantumOps.cpp | 13 ++++++ .../Quantum/Interfaces/CMakeLists.txt | 1 + .../TestDecomposableGateInterface.cpp | 46 +++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 3ddd6b56a1..7067d63a66 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -871,7 +871,8 @@ def PCPhaseOp : UnitaryGate_Op<"pcphase", [DifferentiableGate, NoMemoryEffect, def QubitUnitaryOp : UnitaryGate_Op<"unitary", [ParametrizedGate, NoMemoryEffect, - AttrSizedOperandSegments, AttrSizedResultSegments]> { + AttrSizedOperandSegments, AttrSizedResultSegments, + DeclareOpInterfaceMethods]> { let summary = "Apply an arbitrary fixed unitary matrix"; let description = [{ The `quantum.unitary` operation applies an arbitrary fixed unitary matrix to the diff --git a/mlir/lib/Quantum/IR/QuantumOps.cpp b/mlir/lib/Quantum/IR/QuantumOps.cpp index 0efc3f2307..14ee1127b7 100644 --- a/mlir/lib/Quantum/IR/QuantumOps.cpp +++ b/mlir/lib/Quantum/IR/QuantumOps.cpp @@ -1239,6 +1239,19 @@ mlir::DictionaryAttr GlobalPhaseOp::getStaticData() return mlir::DictionaryAttr::get(getContext(), {}); } +// QubitUnitaryOp + +std::string QubitUnitaryOp::getOperatorName() { return "QubitUnitary"; } + +mlir::TypeRange QubitUnitaryOp::getDynamicShape() { return getAllParams().getTypes(); } + +std::vector QubitUnitaryOp::getWireLens() { return {getNonCtrlQubitOperands().size()}; } + +mlir::DictionaryAttr QubitUnitaryOp::getStaticData() +{ + return mlir::DictionaryAttr::get(getContext(), {}); +} + // OperatorOp std::string OperatorOp::getOperatorName() { return getOpName().str(); } diff --git a/mlir/unittests/Quantum/Interfaces/CMakeLists.txt b/mlir/unittests/Quantum/Interfaces/CMakeLists.txt index b9fd7b294c..4be9679e05 100644 --- a/mlir/unittests/Quantum/Interfaces/CMakeLists.txt +++ b/mlir/unittests/Quantum/Interfaces/CMakeLists.txt @@ -8,4 +8,5 @@ target_link_libraries(CatalystQuantumInterfaceTests PRIVATE ${dialect_libs} MLIRQuantum MLIRQRef + MLIRTestDialect ) diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 5b255f617c..17f8e8c4b6 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -42,6 +42,13 @@ using namespace mlir; using namespace catalyst::quantum; +/// The upstream MLIR Test dialect does not have a header we can include +/// We must declare the registration function, and link to the corresponding upstream target +/// in CMake. +namespace test { +void registerTestDialect(mlir::DialectRegistry &); +} // namespace test + TEST(DecomposableGateInterfaceTests, CustomOp) { std::string moduleStr = R"mlir( @@ -224,6 +231,45 @@ module { ASSERT_EQ(gphase.getGraphOpId(), "GlobalPhase[f64][0]{}"); } +TEST(DecomposableGateInterfaceTests, QubitUnitaryOp) +{ + std::string moduleStr = R"mlir( +module { + %matrix = "test.op"() : () -> tensor<4x4xcomplex> + %q0 = quantum.alloc_qb : !quantum.bit + %q1 = quantum.alloc_qb : !quantum.bit + %q2 = quantum.alloc_qb : !quantum.bit + %oq0, %oq1, %oq2 = quantum.unitary(%matrix : tensor<4x4xcomplex>) %q0, %q1 ctrls(%q2) : !quantum.bit, !quantum.bit ctrls !quantum.bit +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + test::registerTestDialect(registry); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate unitary = *module->getOps().begin(); + + ASSERT_EQ(unitary.getOperatorName(), "QubitUnitary"); + + // This is needed to keep the backing array from being deleted + Type f64type = mlir::Float64Type::get(&context); + Type tensorType = mlir::RankedTensorType::get({4, 4}, mlir::ComplexType::get(f64type)); + llvm::SmallVector backing({tensorType}); + mlir::TypeRange expectedDynamicShape(backing); + ASSERT_EQ(llvm::SmallVector(unitary.getDynamicShape()), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(unitary.getWireLens(), std::vector({2})); + + ASSERT_EQ(unitary.getStaticData().size(), 0); + + ASSERT_EQ(unitary.getGraphOpId(), "QubitUnitary[tensor<4x4xcomplex>][2]{}"); +} + TEST(DecomposableGateInterfaceTests, OperatorOpQubits) { std::string moduleStr = R"mlir( From ec21744067d720ae8797a6cc3c8d62019284dd29 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Wed, 15 Jul 2026 11:14:06 -0400 Subject: [PATCH 53/78] compile test_rules.mlirbc in CI --- Makefile | 1 + mlir/test/lit.cfg.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6f4f3c28e5..cc48b994b6 100644 --- a/Makefile +++ b/Makefile @@ -337,6 +337,7 @@ coverage: coverage-frontend coverage-runtime lit-coverage: @echo "Running lit tests with coverage" + $(DIALECTS_BUILD_DIR)/bin/quantum-opt --emit-bytecode --register-decomp-rule-resource $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlir -o $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlirbc CATALYST_LIBPYTHON=$$($(PYTHON) -c 'from catalyst.utils.runtime_environment import get_libpython_path; print(get_libpython_path())') ENABLE_LIT_COVERAGE=1 COVERAGE_FILE=$(MK_DIR)/.coverage.lit $(PYTHON) $(LLVM_BUILD_DIR)/bin/llvm-lit -sv frontend/test/lit -j$(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) coverage-frontend: diff --git a/mlir/test/lit.cfg.py b/mlir/test/lit.cfg.py index b28ad443d8..9f3c813ad1 100644 --- a/mlir/test/lit.cfg.py +++ b/mlir/test/lit.cfg.py @@ -36,7 +36,6 @@ config.substitutions.append(("%PYTHON", python_executable)) config.substitutions.append(("%BYTECODE_PATH", config.bytecode_path)) -config.excludes.append("test_rules.mlir") # Define PATH to include the various tools needed for our tests. try: From ba092c3a8afd37d9f56e6949d6ac5fcc95faec38 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Wed, 15 Jul 2026 11:25:59 -0400 Subject: [PATCH 54/78] use cmake --- Makefile | 1 - mlir/test/frontend/CMakeLists.txt | 13 +++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index cc48b994b6..6f4f3c28e5 100644 --- a/Makefile +++ b/Makefile @@ -337,7 +337,6 @@ coverage: coverage-frontend coverage-runtime lit-coverage: @echo "Running lit tests with coverage" - $(DIALECTS_BUILD_DIR)/bin/quantum-opt --emit-bytecode --register-decomp-rule-resource $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlir -o $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlirbc CATALYST_LIBPYTHON=$$($(PYTHON) -c 'from catalyst.utils.runtime_environment import get_libpython_path; print(get_libpython_path())') ENABLE_LIT_COVERAGE=1 COVERAGE_FILE=$(MK_DIR)/.coverage.lit $(PYTHON) $(LLVM_BUILD_DIR)/bin/llvm-lit -sv frontend/test/lit -j$(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) coverage-frontend: diff --git a/mlir/test/frontend/CMakeLists.txt b/mlir/test/frontend/CMakeLists.txt index f33509baa3..722542e46d 100644 --- a/mlir/test/frontend/CMakeLists.txt +++ b/mlir/test/frontend/CMakeLists.txt @@ -1,11 +1,20 @@ +set(FRONTEND_LIT_TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../frontend/test/lit") +set(DECOMP_RULES_BYTECODE_PATH "${FRONTEND_LIT_TEST_DIR}/GraphDecomposition/test_rules.mlirbc") + configure_lit_site_cfg( ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py MAIN_CONFIG - ${CMAKE_SOURCE_DIR}/../frontend/test/lit/lit.cfg.py + ${FRONTEND_LIT_TEST_DIR}/lit.cfg.py +) + +add_custom_target(create_test_bytecode + quantum-opt --emit-bytecode --register-decomp-rule-resource ${FRONTEND_LIT_TEST_DIR}/GraphDecomposition/test_rules.mlir -o ${DECOMP_RULES_BYTECODE_PATH} + DEPENDS quantum-opt + BYPRODUCTS ${DECOMP_RULES_BYTECODE_PATH} ) add_lit_testsuite(check-frontend "Run the frontend tests" . # the frontend tests are located in the same directory as the config file - DEPENDS catalyst-cli + DEPENDS catalyst-cli create_test_bytecode ) From 30758e59b02851301743d683e6dfe14508186b72 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Wed, 15 Jul 2026 13:23:01 -0400 Subject: [PATCH 55/78] call frontend lit test camke in `make lit-coverage` --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 6f4f3c28e5..6c48a5f5c2 100644 --- a/Makefile +++ b/Makefile @@ -337,6 +337,7 @@ coverage: coverage-frontend coverage-runtime lit-coverage: @echo "Running lit tests with coverage" + cmake --build $(DIALECTS_BUILD_DIR) --target create_test_bytecode CATALYST_LIBPYTHON=$$($(PYTHON) -c 'from catalyst.utils.runtime_environment import get_libpython_path; print(get_libpython_path())') ENABLE_LIT_COVERAGE=1 COVERAGE_FILE=$(MK_DIR)/.coverage.lit $(PYTHON) $(LLVM_BUILD_DIR)/bin/llvm-lit -sv frontend/test/lit -j$(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) coverage-frontend: From e521133b49069acfddf4ffb43a4ca4288ed0ae39 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Wed, 15 Jul 2026 14:02:02 -0400 Subject: [PATCH 56/78] just use quantum-opt --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6c48a5f5c2..cc48b994b6 100644 --- a/Makefile +++ b/Makefile @@ -337,7 +337,7 @@ coverage: coverage-frontend coverage-runtime lit-coverage: @echo "Running lit tests with coverage" - cmake --build $(DIALECTS_BUILD_DIR) --target create_test_bytecode + $(DIALECTS_BUILD_DIR)/bin/quantum-opt --emit-bytecode --register-decomp-rule-resource $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlir -o $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlirbc CATALYST_LIBPYTHON=$$($(PYTHON) -c 'from catalyst.utils.runtime_environment import get_libpython_path; print(get_libpython_path())') ENABLE_LIT_COVERAGE=1 COVERAGE_FILE=$(MK_DIR)/.coverage.lit $(PYTHON) $(LLVM_BUILD_DIR)/bin/llvm-lit -sv frontend/test/lit -j$(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) coverage-frontend: From 22f899214f942c6fc9c5ea8cb3197b78ac9dd31a Mon Sep 17 00:00:00 2001 From: paul0403 Date: Wed, 15 Jul 2026 14:54:22 -0400 Subject: [PATCH 57/78] use catalyst cli --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cc48b994b6..b9f60f5870 100644 --- a/Makefile +++ b/Makefile @@ -337,7 +337,7 @@ coverage: coverage-frontend coverage-runtime lit-coverage: @echo "Running lit tests with coverage" - $(DIALECTS_BUILD_DIR)/bin/quantum-opt --emit-bytecode --register-decomp-rule-resource $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlir -o $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlirbc + $(DIALECTS_BUILD_DIR)/bin/catalyst --tool=opt --emit-bytecode --register-decomp-rule-resource $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlir > $(MK_DIR)/frontend/test/lit/GraphDecomposition/test_rules.mlirbc CATALYST_LIBPYTHON=$$($(PYTHON) -c 'from catalyst.utils.runtime_environment import get_libpython_path; print(get_libpython_path())') ENABLE_LIT_COVERAGE=1 COVERAGE_FILE=$(MK_DIR)/.coverage.lit $(PYTHON) $(LLVM_BUILD_DIR)/bin/llvm-lit -sv frontend/test/lit -j$(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) coverage-frontend: From 519c5e9d8973acf89335f8c4ed18562688022fa3 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Thu, 16 Jul 2026 09:54:33 -0400 Subject: [PATCH 58/78] pylint --- frontend/catalyst/device/python_decompositions.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 0fdf0ee697..b6a4b1bab0 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -36,7 +36,7 @@ - Is self-contained, and does not contain any device initialization, setup/teardown etc. """ -# pylint: disable=protected-access,unused-argument +# pylint: disable=protected-access,bare-except import jax.numpy as jnp import pennylane as qp @@ -60,6 +60,9 @@ def decomp_rule(*params, wires): # let this fail with the standard error message if the op is not found subroutines = [rule_to_subroutine(rule) for rule in qp.decomposition.list_decomps(op_name)] + # TODO: not all PL ops have been migrated to the operator 2 format expected by mlir graph decomp + # This means some rules will fail the python callback compilation. + # When migration is complete, remove the try-except. try: @qp.qjit( @@ -69,9 +72,9 @@ def decomp_rule(*params, wires): @qp.qnode(device=device) def circuit(): for subroutine in subroutines: - # TODO I know this is dynamic, but we should probably have a better way of handling this - # than hard-coded dummy values. Revisit this when unifying the decomp-rule lowering - # pipeline + # TODO: I know this is dynamic, but we should probably have a better way of handling + # this than hard-coded dummy values. Revisit this when unifying the decomp-rule + # lowering pipeline subroutine(*[0.5 for _ in dynamic_shape], wires=wires) return str(circuit.mlir_module) From 553e504fc81c3ed6d3714fff538017e34e6cac21 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Thu, 16 Jul 2026 11:35:02 -0400 Subject: [PATCH 59/78] print as a tensor --- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 48 ++++++++++++++++++- .../TestDecomposableGateInterface.cpp | 7 ++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index b40fe50ee1..7e4eca210d 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -14,6 +14,7 @@ #include "Quantum/IR/QuantumInterfaces.h" +#include #include #include "llvm/ADT/STLExtras.h" @@ -70,6 +71,51 @@ void printAttr(mlir::Attribute attr, llvm::raw_string_ostream &ss) .Case([&](mlir::FloatAttr attr) { ss << attr.getValueAsDouble(); }) .Default([&](mlir::Attribute attr) { attr.print(ss); }); } + +void printShapedType(ArrayRef shape, int64_t dim, Type elementType, + llvm::raw_string_ostream &ss) +{ + int64_t length = shape[dim]; + auto printList = [&](auto printItem) { + ss << "["; + for (int64_t i = 0; i < length; i++) { + printItem(); + if (i != length - 1) { + ss << ","; + } + } + ss << "]"; + }; + + if (static_cast(shape.size()) == dim + 1) { + printList([&]() { ss << elementType; }); + } + else { + printList([&]() { printShapedType(shape, dim + 1, elementType, ss); }); + } +} + +void printType(mlir::Type type, llvm::raw_string_ostream &ss) +{ + llvm::TypeSwitch(type) + .Case([&](mlir::ShapedType shapedType) { + printShapedType(shapedType.getShape(), 0, shapedType.getElementType(), ss); + }) + .Default([&](mlir::Type other) { other.print(ss); }); +} + +void printTypeRange(mlir::TypeRange typerange, llvm::raw_string_ostream &ss) +{ + ss << "["; + for (auto [i, type] : llvm::enumerate(typerange)) { + if (i > 0) { + ss << ","; + } + printType(type, ss); + } + ss << "]"; +} + } // namespace //===----------------------------------------------------------------------===// @@ -87,7 +133,7 @@ std::string defaultGetGraphOpId(Operation *op) DecomposableGate gate = cast(op); ss << gate.getOperatorName(); - printIterable(gate.getDynamicShape(), ss); + printTypeRange(gate.getDynamicShape(), ss); printIterable(gate.getWireLens(), ss); printAttr(gate.getStaticData(), ss); if (gate.getExtraData() != "") { diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 17f8e8c4b6..002cbd705a 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -267,7 +267,12 @@ module { ASSERT_EQ(unitary.getStaticData().size(), 0); - ASSERT_EQ(unitary.getGraphOpId(), "QubitUnitary[tensor<4x4xcomplex>][2]{}"); + ASSERT_EQ(unitary.getGraphOpId(), "QubitUnitary[" + "[[complex,complex,complex,complex]," + "[complex,complex,complex,complex]," + "[complex,complex,complex,complex]," + "[complex,complex,complex,complex]]" + "][2]{}"); } TEST(DecomposableGateInterfaceTests, OperatorOpQubits) From 66dfb169fb04e03b8868c346aca1ef7a581943fe Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 11:27:08 -0400 Subject: [PATCH 60/78] add support for ids --- .../DecompGraphSolver/DGTypes.hpp | 37 +++++++++++++++++-- .../DecompGraphSolver/DGUtils.hpp | 5 +++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp index 9734285415..db092e74c8 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp @@ -20,7 +20,9 @@ */ #pragma once +#include #include +#include #include #include #include @@ -58,9 +60,16 @@ struct OperatorNode { // Optional static arguments for operators that require additional data. std::unordered_map staticNamedArgs{}; + std::string id{""}; bool operator==(const OperatorNode &other) const { + // id match + if (!id.empty() && !other.id.empty() && id == other.id) { + return true; + } + // legacy fallback if either op is missing ID + // For equality, we consider numWires and numParams conditionally equal // if they are not set to -1 (which indicates a wildcard that can match any value). const bool default_wires = @@ -98,6 +107,10 @@ struct OperatorNode { struct OperatorNodeHash { std::size_t operator()(const OperatorNode &node) const { + // prefer id if available + if (!node.id.empty()) { + return std::hash{}(node.id); + } return std::hash{}(node.name); } }; @@ -108,7 +121,23 @@ struct OperatorNodeHash { struct WeightedGateset { std::unordered_map ops; - [[nodiscard]] bool contains(const OperatorNode &op) const { return ops.find(op) != ops.end(); } + [[nodiscard]] bool contains(const OperatorNode &op) const + { + // hash match + if (ops.find(op) != ops.end()) { + return true; + } + + // use op-matching if hashes failed (could be id vs name) + for (auto [gatesetOp, cost] : ops) { + if (gatesetOp == op) { + return true; + } + } + + return false; + } + [[nodiscard]] double getCost(const OperatorNode &op) const { auto it = ops.find(op); @@ -137,7 +166,8 @@ struct RuleTerm { * @brief This represents the origin of a decomposition rule. * * This enum is used to categorize decomposition rules based on their source or type: - * - Default: The default rule for decomposing an operator as defined in the decomposition graph. + * - Default: The default rule for decomposing an operator as defined in the decomposition + * graph. * - Fixed: A fixed rule that cannot be changed or overridden by the solver. * - Alternative: An alternative rule that can be used in place of the default rule. */ @@ -154,7 +184,8 @@ enum class RuleOrigin : uint8_t { Default = 0, Fixed = 1, Alternative = 2 }; * * TODO: * - We can add a field for work_wires_required if we want to consider the number of ancillary - * wires needed for the decomposition, which can be an important factor in resource optimization. + * wires needed for the decomposition, which can be an important factor in resource + * optimization. * - We can also consider adding a field for the decomposition function or a pointer to it, * which can be used to actually perform the decomposition after the graph solver selects * the rules. diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp index ebab329883..e962638a33 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp @@ -33,6 +33,11 @@ namespace DecompGraph::Core { static inline auto print_op(const OperatorNode &op) -> std::string { + // id override + if (!op.id.empty()) { + return "id: " + op.id; + } + std::ostringstream oss; oss << op.name; oss << "[w:" << op.numWires << "]"; From 1f539f30299584d8e04c7178f520840d583fdf1f Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 11:27:17 -0400 Subject: [PATCH 61/78] add tests --- .../Test_DecompGraphCore.cpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp index 2963378b30..6d5894694d 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp @@ -146,3 +146,45 @@ TEST_CASE("Test ChosenDecompRule construction", "[DecompGraph::Core]") REQUIRE(chosenRule.basisCounts[rz] == 2); REQUIRE(chosenRule.basisCounts[rx] == 1); } + +TEST_CASE("Test graphOpId Support", "[DecompGraph::Core]") +{ + // comparing an op without an ID should fallback to legacy match + const OperatorNode h{"H"}; + const OperatorNode hId{"H", -1, -1, false, {}, "H[][1]{}"}; + + REQUIRE(h == hId); + + // id + legacy match with non-wildcard params + const OperatorNode x{"X", 1, 0}; + const OperatorNode xId{"X", -1, -1, false, {}, "X[][1]{}"}; + + REQUIRE(x == xId); + + // id nodes should fail legacy match if params differ + const OperatorNode op1{"op", 1, 1}; + const OperatorNode op2{"op", 2, 2, false, {}, "op[f64,f64][2]{}"}; + + REQUIRE_FALSE(op1 == op2); + + // nodes with same ids should match + const OperatorNode pr1{"PauliRot", -1, -1, false, {}, "PauliRot[f64][2]{pauli_word:XX}"}; + const OperatorNode pr2{"PauliRot", -1, -1, false, {}, "PauliRot[f64][2]{pauli_word:XX}"}; + + REQUIRE(pr1 == pr2); + + // nodes with matching ids should ignore other parameters (id is source of truth) + const OperatorNode id1{"name1", 1, 1, false, {}, "sameID"}; + const OperatorNode id2{"name2", 2, 2, true, {}, "sameID"}; + + REQUIRE(id1 == id2); + + // check ID hashes match for same IDs, doesn't match name: + const OperatorNode hash1{"name", -1, -1, false, {}, "id"}; + const OperatorNode hash2{"name2", 1, 1, true, {}, "id"}; + const OperatorNode hash3{"name2", 1, 1, true, {}}; + + const OperatorNodeHash hashFunc; + REQUIRE(hashFunc(hash1) == hashFunc(hash2)); + REQUIRE(hashFunc(hash2) != hashFunc(hash3)); +} From cafd58ce644097968183dcff68d23a752c8df714 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 11:46:03 -0400 Subject: [PATCH 62/78] changelog --- doc/releases/changelog-dev.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index a0c22c992e..ad052f13ce 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -251,8 +251,9 @@ of the default else branch. [(#3018)](https://github.com/PennyLaneAI/catalyst/pull/3018) -* Add the `DecomposableGate` op interface to allow generic handling of operations in the `graph-decomposition` pass. This allows arbitrary operations implementing the interface to be registered to and decomposed by the graph. This also allows the use of python-decompositions for any operator pre-registered in the frontend graph. +* Add the `DecomposableGate` op interface to allow generic handling of operations in the `graph-decomposition` pass. This allows arbitrary operations implementing the interface to be registered to and decomposed by the graph. This also allows the use of python-decompositions for any operator pre-registered in the frontend graph. The graph solver now supports the new `graphOpId`s provided by the interface, as well as the legacy pathway with `name`, `numWires` etc. [(#2983)](https://github.com/PennyLaneAI/catalyst/pull/2983) + [(#3039)](https://github.com/PennyLaneAI/catalyst/pull/3039) * The `graph-decomposition` pass eliminates three redundant IR manipulations: the cloning, removal, and re-insertion of user rules. This optimization is particularly From bb70c72d4216555e86968c5adcdaa7207a0e07ad Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 10:58:21 -0400 Subject: [PATCH 63/78] generic decomp rule lowering --- .../catalyst/device/python_decompositions.py | 155 ++++++++++++++---- 1 file changed, 125 insertions(+), 30 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 3edec55384..e7ec674814 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -14,32 +14,97 @@ """ This module provides infrastructure for compile-time lowering of decomposition rules via python. - -Python decomposition wrappers should adhere to the following specifications: - The wrapper: - - Is named `{op name}_decomposition_wrapper`. - - Has a signature identical to the named parameters of the associated PL operator; dynamic - arguments may be unused, but should still be included for compatibility. - - Is able to AOT lower the decomposition rule to MLIR without invoking the compiler, e.g. - using `target="mlir"`, AOT compilation and `QJIT.mlir_module`. - See existing examples for further information. - - Returns a string representation of an MLIR module, containing a FuncOp which represents - the instantiated decomposition rule. - - The FuncOp decomposition rule in the returned string: - - Is named `{op name}_decomp_rule`. - - Is an MLIR representation of the PennyLane decomposition rule associated with the - specified operator. - - Is instantiated with the static data provided, and all other data remains dynamic. - - Is compatible with the `decompose-lowering` pass, i.e. can be mapped to the MLIR operation - it decomposes and inlined. - - Is self-contained, and does not contain any device initialization, setup/teardown etc. """ # pylint: disable=protected-access,unused-argument import jax.numpy as jnp import pennylane as qp +from jax._src.lib.mlir import ir + +from catalyst.jax_extras.lowering import get_mlir_attribute_from_pyval + +_MLIR_DTYPES = { + "i1": jnp.bool_, + "i8": jnp.int8, + "i16": jnp.int16, + "i32": jnp.int32, + "i64": jnp.int64, + "f16": jnp.float16, + "f32": jnp.float32, + "f64": jnp.float64, +} + + +def get_dummy_values_for_container(container): + """Given a container of python types, replace the types with corresponding dummy values.""" + dummy_args = [] + for dtype in container: + if isinstance(dtype, str): + if dtype in _MLIR_DTYPES: + count = 1 + dtype = _MLIR_DTYPES[dtype] + elif dtype.startswith("tensor"): + # tensor<{number}x{type}> + dtype = dtype.removeprefix("tensor<") + dtype = dtype.remove_suffice(">") + count, dtype = dtype.split("x") + else: + raise ValueError(f"Unknown dtype {dtype}.") + else: + count = 1 + dtype = jnp.dtype(dtype) + + dummy_args.append(jnp.zeros((count,), dtype=dtype)) + + return tuple(dummy_args) + + +def get_graph_op_id(op: qp.decomposition.CompressedResourceOp | qp.Operator2): + """ + Return the graph operator id for the operator2 instance `op`. + + The FuncOp decomposition rules in the returned string satisfy the following requirements: + - Are named `{rule name}_{op graph ID}`. + - Are MLIR representations of the PennyLane decomposition rules associated with the + specified operator. + - Are instantiated with the static data provided, and all other data remains dynamic. + - Are self-contained, and do not contain any device initialization, setup/teardown etc. + - Are compatible with the `decompose-lowering` and `graph-decomposition` passes, meaning + the following: + - Their `target_gate` attribute is set to the provided graph operator ID + - They have a resources attribute containing an operations attribute which maps graph + operator IDs to counts of their occurrences in the rule. + - Their arguments are mappable to the operator they decompose via `decompose-lowering`. + + Note that this function should not be updated without updating the corresponding method on the + DecomposableGate interface in mlir/lib/quantum/IR/QuantumInterfaces.cpp. + """ + if isinstance(op, qp.decomposition.CompressedResourceOp): + # NOTE: handling this the old-fashioned way, remove once Operator2 migration is complete + op_type = op.op_type + name = op.op_type.__name__ + num_params = str(op_type.num_params) + num_wires = str(op_type.num_wires) if op_type.num_wires else "0" + return name + "(" + num_params + "," + num_wires + ")" + elif isinstance(op, qp.core.operator.Operator2): + name = op.__name__ + dynamic_shape = op.getDynamicShape() + wire_lens = op.getWireLens() + static_data = op.getStaticData() + extra_data = op.uid + return ( + name + + ("[" + dynamic_shape + "]") + + ("[" + wire_lens + "]") + + ("{" + static_data + "}") + + ("[" + extra_data + "]") + ) + else: + raise ValueError( + "Only AbstractOperator and CompressedResourceOp types are supported for generating a " + f"graph ID, got {op} of type {type(op)}" + ) def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, static_data) -> str: @@ -47,18 +112,27 @@ def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, stati device = qp.device("null.qubit", wires=sum(wire_lens)) wires = tuple(jnp.array(range(length), dtype=int) for length in wire_lens) + decomp_rules = qp.decomposition.list_decomps(op_name) + + # map rules to resource resources, in a more generic format + name_to_resources = { + rule.name: { + get_graph_op_id(op): count + for op, count in rule.compute_resources(**static_data).gate_counts.items() + } + for rule in decomp_rules + } + def rule_to_subroutine(rule): def decomp_rule(*params, wires): rule._impl(*params, *wires, **static_data) - # TODO remove this once we have unified lowering, we should be able to set target_gate and - # stop relying on function names - decomp_rule.__name__ = op_id + "_" + rule.name + # keep the frontend name for readability, append target op_id for symbol uniqueness + decomp_rule.__name__ = rule._impl.__name__ + "_" + op_id return qp.capture.subroutine(decomp_rule) - # let this fail with the standard error message if the op is not found - subroutines = [rule_to_subroutine(rule) for rule in qp.decomposition.list_decomps(op_name)] + subroutines = [rule_to_subroutine(rule) for rule in decomp_rules] @qp.qjit( target="mlir", @@ -67,9 +141,30 @@ def decomp_rule(*params, wires): @qp.qnode(device=device) def circuit(): for subroutine in subroutines: - # TODO I know this is dynamic, but we should probably have a better way of handling this - # than hard-coded dummy values. Revisit this when unifying the decomp-rule lowering - # pipeline - subroutine(*[0.5 for _ in dynamic_shape], wires=wires) + subroutine(*get_dummy_values_for_container(dynamic_shape), wires=wires) + + module = circuit.mlir_module + + def update_funcop_attributes(op): + """Update the decomposition rule attributes if op is a decomposition rule. + + For use with module.walk + + This function updates the following attributes: + - Adds the `target_gate` attribute. + - Adds the `resources` attribute. + """ + if op.name == "func.func": + rule_name = ir.StringAttr(op.attributes["sym_name"]).value.removesuffix("_" + op_id) + if rule_name in name_to_resources: + op.attributes["resources"] = get_mlir_attribute_from_pyval( + {"operations": name_to_resources[rule_name]} + ) + op.attributes["target_gate"] = ir.StringAttr.get(op_id) + + return ir.WalkResult.ADVANCE + + with module.context: + module.operation.walk(update_funcop_attributes) - return str(circuit.mlir_module) + return str(module) From db395d823166d09d462f8f667a2870f2a5a0704f Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 10:58:46 -0400 Subject: [PATCH 64/78] combine tests for rule lowering --- frontend/test/pytest/test_QPD.py | 58 ------------ ..._decomp_rules.py => test_rule_lowering.py} | 91 +++++++++++++------ 2 files changed, 64 insertions(+), 85 deletions(-) delete mode 100644 frontend/test/pytest/test_QPD.py rename frontend/test/pytest/{test_precompile_decomp_rules.py => test_rule_lowering.py} (64%) diff --git a/frontend/test/pytest/test_QPD.py b/frontend/test/pytest/test_QPD.py deleted file mode 100644 index a4986d9d7d..0000000000 --- a/frontend/test/pytest/test_QPD.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2026 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unit tests for the python decompositions module.""" - -import pennylane as qp -import pytest - -from catalyst.device.python_decompositions import python_decomposition_wrapper - - -class TestQPD: - """Test the python wrapper functions used for compile-time decomposition rule lowering.""" - - def test_paulirot_wrapper(self): - """Test that the paulirot QPD wrapper correctly returns the IR as a string.""" - result = python_decomposition_wrapper( - "PauliRot", "PauliRot[f64][3]{pauli_word:XZZ}", [0.4], [3], {"pauli_word": "XZZ"} - ) - assert isinstance(result, str) - assert "PauliRot[f64][3]{pauli_word:XZZ}__pauli_rot_decomposition" in result - assert "Hadamard" in result - assert "multirz" in result - - def test_multiple_rules(self): - """Test that the python decomposition wrapper supports multiple rules.""" - with qp.decomposition.local_decomps(): - - def test_resources(): - return {qp.X: 1} - - @qp.register_resources(test_resources) - def test_decomp(angle, wires, pauli_word): # pylint: disable=unused-argument - qp.RX(angle, wires[0]) - - qp.add_decomps(qp.PauliRot, test_decomp) - - result = python_decomposition_wrapper( - "PauliRot", "PauliRot[f64][3]{pauli_word:XYX}", [float], [3], {"pauli_word": "XYX"} - ) - - assert "PauliRot[f64][3]{pauli_word:XYX}_test_decomp" in result - assert "test_decomp" in result - - -if __name__ == "__main__": - pytest.main(["-x", __file__]) diff --git a/frontend/test/pytest/test_precompile_decomp_rules.py b/frontend/test/pytest/test_rule_lowering.py similarity index 64% rename from frontend/test/pytest/test_precompile_decomp_rules.py rename to frontend/test/pytest/test_rule_lowering.py index f2e78327d6..1df799088b 100644 --- a/frontend/test/pytest/test_precompile_decomp_rules.py +++ b/frontend/test/pytest/test_rule_lowering.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the decomposition rule precompilation utilities.""" +"""Unit tests for the python decompositions module.""" from pathlib import Path @@ -20,6 +20,7 @@ import pytest from catalyst.compiler import _quantum_opt +from catalyst.device.python_decompositions import python_decomposition_wrapper from catalyst.utils.precompile_decomposition_rules import ( compile_op_decomp_rules, get_abstract_args, @@ -28,9 +29,7 @@ from catalyst.utils.runtime_environment import BYTECODE_FILE_PATH -class TestGetAbstractArgs: - """Tests for get_abstract_args.""" - +class TestPrecompiled: def test_ignore_wires(self): """Test that get_abstract_args correctly ignores WiresLike params.""" assert not get_abstract_args(qp.X) @@ -55,8 +54,6 @@ def test_dimension_failure(self): with pytest.raises(ValueError, match="Cannot generate arguments"): get_abstract_args(qp.ControlledQubitUnitary) - -class TestCompileOpDecompRules: """Tests for compile_op_decomp_rules.""" def test_compile_hadamard_rules(self): @@ -126,33 +123,73 @@ def fake_op_decomp(string): compile_op_decomp_rules(NewFakeOp) + def test_bytecode_file(self): + """Test that the bytecode file is generated correctly.""" + orig_bcfile = Path(BYTECODE_FILE_PATH) + tmp_bcfile = None + + if orig_bcfile.exists(): + tmp_bcfile = orig_bcfile.replace(BYTECODE_FILE_PATH + ".tmpbackup") + + try: + precompile_decomp_rules() + assert orig_bcfile.exists() + + finally: + if tmp_bcfile: + tmp_bcfile = tmp_bcfile.replace(orig_bcfile) + else: + orig_bcfile.unlink(missing_ok=True) + + # NOTE: empty pass is needed to prevent running default pipeline + rules = _quantum_opt("--empty", BYTECODE_FILE_PATH) + + assert "_isingxy_to_h_cy" in rules + assert "_doublexcit" in rules + assert "_pauliz_to_ps" in rules + assert "_cphase_to_ppr" in rules + assert "_crot" in rules + + +class TestTraceTime: + pass + + +class TestOnDemand: + """ + Test the python wrapper functions used for on-demand, compile-time decomposition rule lowering. + """ + + def test_paulirot_wrapper(self): + """Test that the paulirot QPD wrapper correctly returns the IR as a string.""" + result = python_decomposition_wrapper( + "PauliRot", "PauliRot[f64][3]{pauli_word:XZZ}", ["i32"], [3], {"pauli_word": "XZZ"} + ) + assert isinstance(result, str) + assert "_pauli_rot_decomposition" in result + assert 'target_gate = "PauliRot[f64][3]{pauli_word:XZZ}"' in result + assert "Hadamard" in result + assert "multirz" in result -def test_bytecode_file(): - """Test that the bytecode file is generated correctly.""" - orig_bcfile = Path(BYTECODE_FILE_PATH) - tmp_bcfile = None + def test_multiple_rules(self): + """Test that the python decomposition wrapper supports multiple rules.""" + with qp.decomposition.local_decomps(): - if orig_bcfile.exists(): - tmp_bcfile = orig_bcfile.replace(BYTECODE_FILE_PATH + ".tmpbackup") + def test_resources(pauli_word): + return {qp.X: 1} - try: - precompile_decomp_rules() - assert orig_bcfile.exists() + @qp.register_resources(test_resources) + def test_decomp(angle, wires, pauli_word): + qp.RX(angle, wires[0]) - finally: - if tmp_bcfile: - tmp_bcfile = tmp_bcfile.replace(orig_bcfile) - else: - orig_bcfile.unlink(missing_ok=True) + qp.add_decomps(qp.PauliRot, test_decomp) - # NOTE: empty pass is needed to prevent running default pipeline - rules = _quantum_opt("--empty", BYTECODE_FILE_PATH) + result = python_decomposition_wrapper( + "PauliRot", "PauliRot[f64][3]{pauli_word:XYX}", ["f64"], [3], {"pauli_word": "XYX"} + ) - assert "_isingxy_to_h_cy" in rules - assert "_doublexcit" in rules - assert "_pauliz_to_ps" in rules - assert "_cphase_to_ppr" in rules - assert "_crot" in rules + assert "test_decomp" in result + assert 'target_gate = "PauliRot[f64][3]{pauli_word:XYX}"' if __name__ == "__main__": From 4816e3207bc3f6cdfec69bc35992abee8b865dc6 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 10:59:35 -0400 Subject: [PATCH 65/78] improve interface type handling --- mlir/lib/Quantum/IR/QuantumInterfaces.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp index b40fe50ee1..acf25df553 100644 --- a/mlir/lib/Quantum/IR/QuantumInterfaces.cpp +++ b/mlir/lib/Quantum/IR/QuantumInterfaces.cpp @@ -21,6 +21,10 @@ #include "llvm/Support/raw_ostream.h" #include "mlir/IR/Attributes.h" #include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/TypeRange.h" +#include "mlir/IR/Types.h" #include "mlir/Support/LLVM.h" using namespace mlir; From ebf3c5c5265824ec984216c4c5e0c1c91f5a16e4 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 11:02:40 -0400 Subject: [PATCH 66/78] update test for improve type handling --- .../TestDecomposableGateInterface.cpp | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp index 27e95736d7..b076f16df1 100644 --- a/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp +++ b/mlir/unittests/Quantum/Interfaces/TestDecomposableGateInterface.cpp @@ -14,12 +14,15 @@ // limitations under the License. #include +#include #include #include #include "gtest/gtest.h" #include "llvm/ADT/SmallVector.h" #include "llvm/Support/Format.h" // for gtest printing on failure +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/raw_ostream.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/IR/AsmState.h" @@ -34,6 +37,7 @@ #include "mlir/IR/TypeRange.h" #include "mlir/IR/Types.h" #include "mlir/Parser/Parser.h" +#include "mlir/Support/LLVM.h" #include "Quantum/IR/QuantumDialect.h" #include "Quantum/IR/QuantumInterfaces.h" @@ -244,3 +248,46 @@ func.func @testfunc(%first : tensor<1xi64>, %secondthird : tensor<2xi64>) { ASSERT_EQ(op.getGraphOpId(), "testOperatorUID[i1,f64,i64][1,2]{}[248]"); } + +TEST(DecomposableGateInterfaceTests, OperatorOpNestedArgs) +{ + std::string moduleStr = R"mlir( +func.func @testfunc(%arg: tensor<3xi64>, %arg2: tensor<2xf64>) { + + %reg = quantum.alloc(4) : !quantum.reg + %q0 = quantum.extract %reg[0] : !quantum.reg -> !quantum.bit + + %0 = quantum.operator "testNestedArgs"(%arg : tensor<3xi64>, %arg2: tensor<2xf64>) + qubits(%q0) + return +} + )mlir"; + + // Parsing boilerplate + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + ParserConfig config(&context, /*verifyAfterParse=*/false); + OwningOpRef module = parseSourceString(moduleStr, config); + + DecomposableGate op; + module->walk([&](OperatorOp walkOp) { op = walkOp; }); + + ASSERT_EQ(op.getOperatorName(), "testNestedArgs"); + + // This is needed to keep the backing array from being deleted + mlir::Type int64 = mlir::IntegerType::get(&context, 64); + llvm::SmallVector backing( + {mlir::RankedTensorType::get(ArrayRef({3}), int64), + mlir::RankedTensorType::get(ArrayRef({2}), mlir::Float64Type::get(&context))}); + mlir::TypeRange expectedDynamicShape(backing); + mlir::TypeRange actual(op.getDynamicShape()); + ASSERT_EQ(llvm::SmallVector(actual), + llvm::SmallVector(expectedDynamicShape)); + + ASSERT_EQ(op.getWireLens(), std::vector({1})); + + ASSERT_EQ(op.getStaticData(), mlir::DictionaryAttr::get(&context, {})); + + ASSERT_EQ(op.getGraphOpId(), "testNestedArgs[[i64,i64,i64],[f64,f64]][1]{}"); +} From 65d3b1211e4e32bd03c1051fd0916b40aed1046a Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 11:02:51 -0400 Subject: [PATCH 67/78] support multiple rules --- .../Transforms/GraphDecomposition/graph_decomposition.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp index 39929153a4..9771f1e544 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp @@ -397,12 +397,11 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBasewalk([&](mlir::func::FuncOp func) { - if (func.getName().starts_with(opId)) { + if (func->hasAttr("target_gate")) { mlir::OwningOpRef outOp; func->remove(); outOp = mlir::OwningOpRef(func); mlir::func::FuncOp funcOp = outOp.get(); - funcOp->setAttr("target_gate", mlir::StringAttr::get(context, opId)); // if we fail to add one of the decomps, we still want to try for the rest std::ignore = addRuleNode(funcOp, ruleNodes); From 6a732920589ae6b943557c16221f397fcd6e4fda Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Tue, 14 Jul 2026 11:53:23 -0400 Subject: [PATCH 68/78] codefactor --- frontend/test/pytest/test_rule_lowering.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/test/pytest/test_rule_lowering.py b/frontend/test/pytest/test_rule_lowering.py index 1df799088b..75f459fb5b 100644 --- a/frontend/test/pytest/test_rule_lowering.py +++ b/frontend/test/pytest/test_rule_lowering.py @@ -30,6 +30,10 @@ class TestPrecompiled: + """Tests for precompiled decomposition rules.""" + + # Tests for get_abstract_args helper + def test_ignore_wires(self): """Test that get_abstract_args correctly ignores WiresLike params.""" assert not get_abstract_args(qp.X) @@ -54,7 +58,7 @@ def test_dimension_failure(self): with pytest.raises(ValueError, match="Cannot generate arguments"): get_abstract_args(qp.ControlledQubitUnitary) - """Tests for compile_op_decomp_rules.""" + # Tests for compile_op_decomp_rules helper def test_compile_hadamard_rules(self): """Test that compile_op_decomp_rules successfully compiles each decomp rule for Hadamard.""" @@ -117,12 +121,14 @@ class NewFakeOp(qp.operation.Operator): @qp.register_resources({}) def fake_op_decomp(string): - qp.PauliRot(2, string) + qp.PauliRot(2, string, list(range(len(string)))) qp.add_decomps(NewFakeOp, fake_op_decomp) compile_op_decomp_rules(NewFakeOp) + # bytecode file tests + def test_bytecode_file(self): """Test that the bytecode file is generated correctly.""" orig_bcfile = Path(BYTECODE_FILE_PATH) @@ -152,6 +158,8 @@ def test_bytecode_file(self): class TestTraceTime: + """Placeholder for future tests of trace-time decomposition rule lowering.""" + pass @@ -160,8 +168,8 @@ class TestOnDemand: Test the python wrapper functions used for on-demand, compile-time decomposition rule lowering. """ - def test_paulirot_wrapper(self): - """Test that the paulirot QPD wrapper correctly returns the IR as a string.""" + def test_paulirot(self): + """Test that the QPD wrapper correctly returns the IR as a string.""" result = python_decomposition_wrapper( "PauliRot", "PauliRot[f64][3]{pauli_word:XZZ}", ["i32"], [3], {"pauli_word": "XZZ"} ) @@ -175,11 +183,11 @@ def test_multiple_rules(self): """Test that the python decomposition wrapper supports multiple rules.""" with qp.decomposition.local_decomps(): - def test_resources(pauli_word): + def test_resources(pauli_word): # pylint: disable=unused-argument return {qp.X: 1} @qp.register_resources(test_resources) - def test_decomp(angle, wires, pauli_word): + def test_decomp(angle, wires, pauli_word): # pylint: disable=unused-argument qp.RX(angle, wires[0]) qp.add_decomps(qp.PauliRot, test_decomp) @@ -189,7 +197,7 @@ def test_decomp(angle, wires, pauli_word): ) assert "test_decomp" in result - assert 'target_gate = "PauliRot[f64][3]{pauli_word:XYX}"' + assert 'target_gate = "PauliRot[f64][3]{pauli_word:XYX}"' in result if __name__ == "__main__": From e5e206a1155c934ab09dbc09f0450ec908e27a12 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 15 Jul 2026 14:22:34 -0400 Subject: [PATCH 69/78] platform-dependent suffix for fallback pathway --- frontend/catalyst/utils/runtime_environment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/catalyst/utils/runtime_environment.py b/frontend/catalyst/utils/runtime_environment.py index fe2b049eec..cad70339e5 100644 --- a/frontend/catalyst/utils/runtime_environment.py +++ b/frontend/catalyst/utils/runtime_environment.py @@ -97,6 +97,7 @@ def get_libpython_path() -> str: # pragma: no cover # conda envs) report the static archive ``libpythonX.Y.a`` there even though a ``.so`` # is present alongside it. We accept ``LDLIBRARY`` only when it is a shared object # that exists, and otherwise fall back to locating the shared library in ``LIBDIR``. + shlib_suffix = ".dylib" if sys.platform == "darwin" else ".so" ldversion = sysconfig.get_config_var("LDVERSION") or sysconfig.get_config_var("VERSION") or "" shlib_suffix = ".dylib" if sys.platform == "darwin" else ".so" conventional_path = os.path.join(libdir, f"libpython{ldversion}{shlib_suffix}") From d595ea9f63449ca03d381e4a092e840beaab7377 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 15 Jul 2026 14:28:31 -0400 Subject: [PATCH 70/78] cleanup --- frontend/catalyst/utils/runtime_environment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/catalyst/utils/runtime_environment.py b/frontend/catalyst/utils/runtime_environment.py index cad70339e5..fe2b049eec 100644 --- a/frontend/catalyst/utils/runtime_environment.py +++ b/frontend/catalyst/utils/runtime_environment.py @@ -97,7 +97,6 @@ def get_libpython_path() -> str: # pragma: no cover # conda envs) report the static archive ``libpythonX.Y.a`` there even though a ``.so`` # is present alongside it. We accept ``LDLIBRARY`` only when it is a shared object # that exists, and otherwise fall back to locating the shared library in ``LIBDIR``. - shlib_suffix = ".dylib" if sys.platform == "darwin" else ".so" ldversion = sysconfig.get_config_var("LDVERSION") or sysconfig.get_config_var("VERSION") or "" shlib_suffix = ".dylib" if sys.platform == "darwin" else ".so" conventional_path = os.path.join(libdir, f"libpython{ldversion}{shlib_suffix}") From dd4121e752716002bad53e49f45db581c60e21c1 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 15 Jul 2026 16:00:33 -0400 Subject: [PATCH 71/78] integrate with precompilation --- .../catalyst/device/python_decompositions.py | 32 ++-- .../utils/precompile_decomposition_rules.py | 151 ++++++------------ 2 files changed, 72 insertions(+), 111 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index e7ec674814..12c8a62e6c 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -21,6 +21,7 @@ import jax.numpy as jnp import pennylane as qp from jax._src.lib.mlir import ir +from jaxlib.mlir.dialects.builtin import ModuleOp from catalyst.jax_extras.lowering import get_mlir_attribute_from_pyval @@ -83,16 +84,21 @@ def get_graph_op_id(op: qp.decomposition.CompressedResourceOp | qp.Operator2): if isinstance(op, qp.decomposition.CompressedResourceOp): # NOTE: handling this the old-fashioned way, remove once Operator2 migration is complete op_type = op.op_type - name = op.op_type.__name__ + else: + op_type = op + + if issubclass(op_type, qp.core.operator.Operator): + name = op_type.__name__ num_params = str(op_type.num_params) num_wires = str(op_type.num_wires) if op_type.num_wires else "0" return name + "(" + num_params + "," + num_wires + ")" - elif isinstance(op, qp.core.operator.Operator2): - name = op.__name__ - dynamic_shape = op.getDynamicShape() - wire_lens = op.getWireLens() - static_data = op.getStaticData() - extra_data = op.uid + elif isinstance(op_type, qp.core.operator.Operator2): + # TODO: use real getters here + name = op_type.__name__ + dynamic_shape = op_type.getDynamicShape() + wire_lens = op_type.getWireLens() + static_data = op_type.getStaticData() + extra_data = op_type.uid return ( name + ("[" + dynamic_shape + "]") @@ -107,8 +113,9 @@ def get_graph_op_id(op: qp.decomposition.CompressedResourceOp | qp.Operator2): ) -def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, static_data) -> str: - """Generic decomposition wrapper.""" +def python_decomposition(op_name, op_id, dynamic_shape, wire_lens, static_data) -> ModuleOp: + """Python decomposition rule lowering.""" + # TODO update docstring device = qp.device("null.qubit", wires=sum(wire_lens)) wires = tuple(jnp.array(range(length), dtype=int) for length in wire_lens) @@ -167,4 +174,9 @@ def update_funcop_attributes(op): with module.context: module.operation.walk(update_funcop_attributes) - return str(module) + return module + + +def python_decomposition_wrapper(op_name, op_id, dynamic_shape, wire_lens, static_data) -> str: + """Generic decomposition wrapper.""" + return str(python_decomposition(op_name, op_id, dynamic_shape, wire_lens, static_data)) diff --git a/frontend/catalyst/utils/precompile_decomposition_rules.py b/frontend/catalyst/utils/precompile_decomposition_rules.py index 2ca103944a..f8f80ec1ee 100644 --- a/frontend/catalyst/utils/precompile_decomposition_rules.py +++ b/frontend/catalyst/utils/precompile_decomposition_rules.py @@ -20,16 +20,16 @@ import jax import pennylane as qp from jax._src.lib.mlir import ir -from pennylane.operation import Operator +from jaxlib.mlir.dialects.builtin import ModuleOp +from pennylane.operation import Operator, Operator2 from catalyst.compiler import _quantum_opt +from catalyst.device.python_decompositions import get_graph_op_id, python_decomposition from catalyst.jax_primitives import decomposition_rule from catalyst.utils.exceptions import CompileError from catalyst.utils.runtime_environment import BYTECODE_FILE_PATH # TODO: Uncomment dynamic size wires ops once they are supported -# FIXME: Use the Gate class instead of this list of compiler ops -# https://github.com/PennyLaneAI/pennylane/pull/8767 COMPILER_OPS_FOR_DECOMPOSITION = { qp.CNOT, qp.ControlledPhaseShift, @@ -96,113 +96,52 @@ def get_abstract_args(op_class: type[Operator]) -> list[type]: return [float for _ in range(op_class.num_params)] -def get_func_from_circuit(module) -> str | None: +def get_rules_from_module(module) -> str: """ - Get the string representation of `rule_wrapper` from module, if it exists. + Parse and modify decomposition rules from a ModuleOp. Args: module: an MLIR module object containing a FuncOp named `rule_wrapper` to be extracted Returns: - str: string representation of FuncOp named `rule_wrapper` from module - None: if no such FuncOp can be found + str: The string representation of any decomposition rules from `module`, pre-pending the + `__builtin_` prefix to their names. """ - decomp_func_op = None + funcOps = [] def find_condition(op): - nonlocal decomp_func_op if op.name == "func.func": - if ir.StringAttr(op.attributes["sym_name"]).value == "rule_wrapper": - decomp_func_op = op - return ir.WalkResult.INTERRUPT + if "target_gate" in op.attributes: + op.attributes["sym_name"] = ir.StringAttr.get( + "__builtin_" + str(op.attributes["sym_name"]) + ) + funcOps.append(op) + return ir.WalkResult.SKIP return ir.WalkResult.ADVANCE module.operation.walk(find_condition) - return str(decomp_func_op) + "\n" if decomp_func_op else None - - -def compile_rule( - op_class, - abstract_args, - op_num_wires, - rule, - dev, -) -> str | None: - """ - Get the string representation of a compiled rule from a python decomposition rule, if possible. - - NOTE: rules with string params are not currently supported. - - Args: - op_class: A PennyLane class subclassing Operation - op_num_wires: the number of wires used by op_class - rule (DecompositionRule): the decomposition rule to be compiled - dev (Device): a device for qjit - - Returns: - str: string representation of the mlir of the decomposition rule. - """ - qp.decomposition.enable_graph() - - # WARNING: do not rename this function, we use it to extract the rule from the compiled - # circuit - @decomposition_rule(is_qreg=True, op_type=op_class.__name__) - def rule_wrapper(*args, wires, **_): - return rule(*args, wires=wires, **_) - - @qp.qjit(capture=True, target="mlir") - @qp.qnode(dev) - def circuit(): - rule_wrapper(*abstract_args, wires=jax.core.ShapedArray((op_num_wires,), int)) - return qp.probs() - - return get_func_from_circuit(circuit.mlir_module) - - -def compile_op_decomp_rules( - op_class: type[Operator], -) -> dict[str, str | None]: - """ - Compile all decomposition rules for op_class. - - Note: the modules include the full circuit IR. - - Args: - op_class (type[Operator]): the op class to compile decomposition rules for. - - Returns: - dict[str, str | None]: decomposition rule names to compiled mlir modules. - """ - op_decomp_rules = qp.decomposition.decomposition_graph.list_decomps(op_class) - - mlir_modules: dict[str, str | None] = {} - - if not hasattr(op_class, "num_wires") or not op_class.num_wires: - warnings.warn( - f"Cannot compile decomposition rules for op {op_class.__name__} with an unknown number " - + "of wires." + return "\n".join(str(funcOp) for funcOp in funcOps) if funcOps else "" + + +def parse_operator_data(op): + """Parse operator data from an Operator/Operator2 instance.""" + if isinstance(op, Operator2): + # TODO: use real getters here + dynamic_shape = op.getDynamicShape() + wire_lens = op.getWireLens() + static_data = op.getStaticData() + return dynamic_shape, wire_lens, static_data + if issubclass(op, Operator): + # NOTE: handling this the old-fashioned way, remove once Operator2 migration is complete + dynamic_shape = get_abstract_args(op) + num_wires = op.num_wires if op.num_wires else 0 + return dynamic_shape, [num_wires], {} + else: + raise ValueError( + "Only AbstractOperator and CompressedResourceOp types are supported for generating a " + f"graph ID, got {op} of type {type(op)}" ) - return mlir_modules - - dev = qp.device("null.qubit", wires=op_class.num_wires) - - abstract_args = get_abstract_args(op_class) # pylint: disable=protected-access - - for rule in op_decomp_rules: - try: - rule_name = rule._impl.__name__ # pylint: disable=protected-access - mlir_modules[rule_name] = compile_rule( - op_class, abstract_args, op_class.num_wires, rule, dev - ) - except CompileError as e: - warnings.warn(f"Failed to compile {rule_name}: {e}") - except Exception as e: # pylint: disable=broad-exception-caught - warnings.warn(f"Unexpected error while trying to compile {rule_name}: {e}") - finally: - qp.decomposition.disable_graph() - - return mlir_modules def precompile_decomp_rules(decomp_file_path: str = BYTECODE_FILE_PATH): @@ -216,11 +155,21 @@ def precompile_decomp_rules(decomp_file_path: str = BYTECODE_FILE_PATH): """ Path(decomp_file_path).parent.mkdir(parents=True, exist_ok=True) - mlir_rules = "".join( - str(mlir).replace("@rule_wrapper", f"@__builtin_{name}") - for func in COMPILER_OPS_FOR_DECOMPOSITION - for name, mlir in compile_op_decomp_rules(func).items() - ) + bytecode_lib = "" + + with ir.Context(): + # TODO: update this for Operator2, PL will implement a precompilation registry + for op in COMPILER_OPS_FOR_DECOMPOSITION: + dynamic_data, wire_lens, static_data = parse_operator_data(op) + if static_data: + # we cannot precompile if the rule takes static data + continue + + mlir_rules = python_decomposition( + op.__name__, get_graph_op_id(op), dynamic_data, wire_lens, {} + ) + + bytecode_lib += get_rules_from_module(mlir_rules) bytecode = _quantum_opt( "--emit-bytecode", @@ -228,7 +177,7 @@ def precompile_decomp_rules(decomp_file_path: str = BYTECODE_FILE_PATH): "--convert-to-value-semantics", "--canonicalize", "--register-decomp-rule-resource", - stdin=mlir_rules.encode("utf-8"), + stdin=bytecode_lib.encode("utf-8"), text=None, ) From 122b0a491cb8a7d04af76eee755258e3b60adfe1 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 15 Jul 2026 16:24:33 -0400 Subject: [PATCH 72/78] update tests --- frontend/test/pytest/test_rule_lowering.py | 83 ++-------------------- 1 file changed, 7 insertions(+), 76 deletions(-) diff --git a/frontend/test/pytest/test_rule_lowering.py b/frontend/test/pytest/test_rule_lowering.py index 75f459fb5b..6515b32940 100644 --- a/frontend/test/pytest/test_rule_lowering.py +++ b/frontend/test/pytest/test_rule_lowering.py @@ -22,7 +22,6 @@ from catalyst.compiler import _quantum_opt from catalyst.device.python_decompositions import python_decomposition_wrapper from catalyst.utils.precompile_decomposition_rules import ( - compile_op_decomp_rules, get_abstract_args, precompile_decomp_rules, ) @@ -58,76 +57,7 @@ def test_dimension_failure(self): with pytest.raises(ValueError, match="Cannot generate arguments"): get_abstract_args(qp.ControlledQubitUnitary) - # Tests for compile_op_decomp_rules helper - - def test_compile_hadamard_rules(self): - """Test that compile_op_decomp_rules successfully compiles each decomp rule for Hadamard.""" - rules = compile_op_decomp_rules(qp.H) - - assert "_hadamard_to_rz_rx" in rules - assert "_hadamard_to_rz_ry" in rules - - def test_compile_rx_rules(self): - """Test that compile_op_decomp_rules successfully compiles each decomp rule for RX gates.""" - rules = compile_op_decomp_rules(qp.RX) - - assert "_rx_to_rot" in rules - assert "_rx_to_rz_ry" in rules - assert "_rx_to_ry_cliff" in rules - assert "_rx_to_rz_cliff" in rules - assert "_rx_to_ppr" in rules - - def test_fails_with_unknown_wires(self): - """ - Test that compile_op_decomp_rules warns when the number of wires is unknown. - """ - with pytest.warns(): - compile_op_decomp_rules(qp.Identity) - - def test_compile_error(self): - """Test that compile_op_decomp_rules warns when compilation of a rule fails.""" - - with pytest.warns(match="Failed to compile"): - with qp.decomposition.local_decomps(): - - class FakeOp(qp.operation.Operator): - """Test class with incompatible decomp rule.""" - - num_wires = 3 - num_params = 1 - ndim_params = (0,) - - @qp.register_resources({}) - def fake_op_decomp(param, wires): - _quantum_opt(stdin="module {") - return param, wires - - qp.add_decomps(FakeOp, fake_op_decomp) - - compile_op_decomp_rules(FakeOp) - - def test_unexpected_error(self): - """Test that compile_op_decomp_rules warns when an unexpected exception is thrown.""" - - with pytest.warns(match="Unexpected error"): - - with qp.decomposition.local_decomps(): - - class NewFakeOp(qp.operation.Operator): - """Test class without ndim_params.""" - - num_wires = 1 - num_params = 1 - - @qp.register_resources({}) - def fake_op_decomp(string): - qp.PauliRot(2, string, list(range(len(string)))) - - qp.add_decomps(NewFakeOp, fake_op_decomp) - - compile_op_decomp_rules(NewFakeOp) - - # bytecode file tests + # Test for bytecode file def test_bytecode_file(self): """Test that the bytecode file is generated correctly.""" @@ -150,11 +80,11 @@ def test_bytecode_file(self): # NOTE: empty pass is needed to prevent running default pipeline rules = _quantum_opt("--empty", BYTECODE_FILE_PATH) - assert "_isingxy_to_h_cy" in rules - assert "_doublexcit" in rules - assert "_pauliz_to_ps" in rules - assert "_cphase_to_ppr" in rules - assert "_crot" in rules + assert "__builtin__isingxy_to_h_cy" in rules + assert "__builtin__doublexcit" in rules + assert "__builtin__pauliz_to_ps" in rules + assert "__builtin__cphase_to_ppr" in rules + assert "__builtin__crot" in rules class TestTraceTime: @@ -197,6 +127,7 @@ def test_decomp(angle, wires, pauli_word): # pylint: disable=unused-argument ) assert "test_decomp" in result + assert "_pauli_rot_decomp" in result assert 'target_gate = "PauliRot[f64][3]{pauli_word:XYX}"' in result From 3eca934ed4fdc156f24af6ececa5d806cae4153b Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 15 Jul 2026 16:38:41 -0400 Subject: [PATCH 73/78] support complex types --- .../catalyst/device/python_decompositions.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 12c8a62e6c..d2a3b68013 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -34,6 +34,8 @@ "f16": jnp.float16, "f32": jnp.float32, "f64": jnp.float64, + "complex": jnp.complex64, + "complex": jnp.complex128, } @@ -141,16 +143,20 @@ def decomp_rule(*params, wires): subroutines = [rule_to_subroutine(rule) for rule in decomp_rules] - @qp.qjit( - target="mlir", - capture=True, - ) - @qp.qnode(device=device) - def circuit(): - for subroutine in subroutines: - subroutine(*get_dummy_values_for_container(dynamic_shape), wires=wires) + try: - module = circuit.mlir_module + @qp.qjit( + target="mlir", + capture=True, + ) + @qp.qnode(device=device) + def circuit(): + for subroutine in subroutines: + subroutine(*get_dummy_values_for_container(dynamic_shape), wires=wires) + + module = circuit.mlir_module + except: + return ModuleOp() def update_funcop_attributes(op): """Update the decomposition rule attributes if op is a decomposition rule. From f68236796cfeb6568b0645fb97316da32a112805 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Wed, 15 Jul 2026 17:19:34 -0400 Subject: [PATCH 74/78] try/except on resources --- .../catalyst/device/python_decompositions.py | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index d2a3b68013..05315255bf 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -121,16 +121,19 @@ def python_decomposition(op_name, op_id, dynamic_shape, wire_lens, static_data) device = qp.device("null.qubit", wires=sum(wire_lens)) wires = tuple(jnp.array(range(length), dtype=int) for length in wire_lens) - decomp_rules = qp.decomposition.list_decomps(op_name) + decomp_rules = list(qp.decomposition.list_decomps(op_name)) # map rules to resource resources, in a more generic format - name_to_resources = { - rule.name: { - get_graph_op_id(op): count - for op, count in rule.compute_resources(**static_data).gate_counts.items() - } - for rule in decomp_rules - } + + name_to_resources = {} + for rule in decomp_rules: + try: + name_to_resources[rule.name] = { + get_graph_op_id(op): count + for op, count in rule.compute_resources(**static_data).gate_counts.items() + } + except: # pylint: disable=bare-except + decomp_rules.remove(rule) def rule_to_subroutine(rule): def decomp_rule(*params, wires): @@ -143,20 +146,16 @@ def decomp_rule(*params, wires): subroutines = [rule_to_subroutine(rule) for rule in decomp_rules] - try: + @qp.qjit( + target="mlir", + capture=True, + ) + @qp.qnode(device=device) + def circuit(): + for subroutine in subroutines: + subroutine(*get_dummy_values_for_container(dynamic_shape), wires=wires) - @qp.qjit( - target="mlir", - capture=True, - ) - @qp.qnode(device=device) - def circuit(): - for subroutine in subroutines: - subroutine(*get_dummy_values_for_container(dynamic_shape), wires=wires) - - module = circuit.mlir_module - except: - return ModuleOp() + module = circuit.mlir_module def update_funcop_attributes(op): """Update the decomposition rule attributes if op is a decomposition rule. From 14cfd6dae3fc938e028d9e1aedd03dbf006c7783 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Thu, 16 Jul 2026 15:14:22 -0400 Subject: [PATCH 75/78] improve id handling --- .../catalyst/device/python_decompositions.py | 4 ++-- .../utils/precompile_decomposition_rules.py | 4 ++-- .../GraphDecomposition/graph_decomposition.cpp | 17 ++--------------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/frontend/catalyst/device/python_decompositions.py b/frontend/catalyst/device/python_decompositions.py index 05315255bf..8075ba6376 100644 --- a/frontend/catalyst/device/python_decompositions.py +++ b/frontend/catalyst/device/python_decompositions.py @@ -91,9 +91,9 @@ def get_graph_op_id(op: qp.decomposition.CompressedResourceOp | qp.Operator2): if issubclass(op_type, qp.core.operator.Operator): name = op_type.__name__ - num_params = str(op_type.num_params) + num_params = op_type.num_params num_wires = str(op_type.num_wires) if op_type.num_wires else "0" - return name + "(" + num_params + "," + num_wires + ")" + return name + "[" + ",".join(["f64"] * num_params) + "][" + num_wires + "]{}" elif isinstance(op_type, qp.core.operator.Operator2): # TODO: use real getters here name = op_type.__name__ diff --git a/frontend/catalyst/utils/precompile_decomposition_rules.py b/frontend/catalyst/utils/precompile_decomposition_rules.py index f8f80ec1ee..ed87eed0c1 100644 --- a/frontend/catalyst/utils/precompile_decomposition_rules.py +++ b/frontend/catalyst/utils/precompile_decomposition_rules.py @@ -113,7 +113,7 @@ def find_condition(op): if op.name == "func.func": if "target_gate" in op.attributes: op.attributes["sym_name"] = ir.StringAttr.get( - "__builtin_" + str(op.attributes["sym_name"]) + "__builtin_" + op.attributes["sym_name"].value.strip('"') ) funcOps.append(op) return ir.WalkResult.SKIP @@ -169,7 +169,7 @@ def precompile_decomp_rules(decomp_file_path: str = BYTECODE_FILE_PATH): op.__name__, get_graph_op_id(op), dynamic_data, wire_lens, {} ) - bytecode_lib += get_rules_from_module(mlir_rules) + bytecode_lib += get_rules_from_module(mlir_rules) + "\n" bytecode = _quantum_opt( "--emit-bytecode", diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp index 9771f1e544..3e75053365 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp @@ -421,7 +421,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(op.getOperation())) { - node.name = customOp.getGateName().str(); - } - // Name handling for non-custom ops - else { - std::string name = op->getName().stripDialect().str(); - if (name == "gphase") { - name = "GlobalPhase"; - } - else if (name == "paulirot") { - name = cast(op.getOperation()).getGraphOpId(); - } - node.name = name; - } + node.name = op.getGraphOpId(); if (auto paramOp = llvm::dyn_cast(op.getOperation())) { From db627c2b2c2dd9284ce7d8543c40327e0aa754b6 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 21 Jul 2026 11:33:15 -0400 Subject: [PATCH 76/78] git whoops --- mlir/include/Quantum/IR/QuantumOps.td | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index c9be9f751a..9848df0723 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -859,9 +859,8 @@ def PCPhaseOp : UnitaryGate_Op<"pcphase", [DifferentiableGate, NoMemoryEffect, }]; let extraClassDeclaration = extraBaseClassDeclaration # [{ - mlir::OperandRange getAllParams() { - return getOperands().slice(getParamOperandIdx(), getParamOperandIdx()+2); - } + mlir::ValueRange getAllParams() { + return getODSOperands(getParamOperandIdx()); }]; let hasCanonicalizeMethod = 1; From 79300d07d99dadd8469c3b6fdd2803252937afb9 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 21 Jul 2026 11:34:05 -0400 Subject: [PATCH 77/78] git whoops again --- mlir/include/Quantum/IR/QuantumOps.td | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/include/Quantum/IR/QuantumOps.td b/mlir/include/Quantum/IR/QuantumOps.td index 9848df0723..e21dac67f9 100644 --- a/mlir/include/Quantum/IR/QuantumOps.td +++ b/mlir/include/Quantum/IR/QuantumOps.td @@ -861,6 +861,7 @@ def PCPhaseOp : UnitaryGate_Op<"pcphase", [DifferentiableGate, NoMemoryEffect, let extraClassDeclaration = extraBaseClassDeclaration # [{ mlir::ValueRange getAllParams() { return getODSOperands(getParamOperandIdx()); + } }]; let hasCanonicalizeMethod = 1; From c295f78c54a2ecf35f234db2fee83ceabc9c1af7 Mon Sep 17 00:00:00 2001 From: paul0403 Date: Tue, 21 Jul 2026 11:36:56 -0400 Subject: [PATCH 78/78] codefactor --- frontend/catalyst/utils/precompile_decomposition_rules.py | 5 ----- frontend/test/pytest/test_rule_lowering.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/frontend/catalyst/utils/precompile_decomposition_rules.py b/frontend/catalyst/utils/precompile_decomposition_rules.py index ed87eed0c1..43cfd52901 100644 --- a/frontend/catalyst/utils/precompile_decomposition_rules.py +++ b/frontend/catalyst/utils/precompile_decomposition_rules.py @@ -14,19 +14,14 @@ """Utilities for AOT compiling PennyLane's decomposition rules to MLIR Bytecode.""" -import warnings from pathlib import Path -import jax import pennylane as qp from jax._src.lib.mlir import ir -from jaxlib.mlir.dialects.builtin import ModuleOp from pennylane.operation import Operator, Operator2 from catalyst.compiler import _quantum_opt from catalyst.device.python_decompositions import get_graph_op_id, python_decomposition -from catalyst.jax_primitives import decomposition_rule -from catalyst.utils.exceptions import CompileError from catalyst.utils.runtime_environment import BYTECODE_FILE_PATH # TODO: Uncomment dynamic size wires ops once they are supported diff --git a/frontend/test/pytest/test_rule_lowering.py b/frontend/test/pytest/test_rule_lowering.py index 6515b32940..6cd00b5fab 100644 --- a/frontend/test/pytest/test_rule_lowering.py +++ b/frontend/test/pytest/test_rule_lowering.py @@ -90,8 +90,6 @@ def test_bytecode_file(self): class TestTraceTime: """Placeholder for future tests of trace-time decomposition rule lowering.""" - pass - class TestOnDemand: """