diff --git a/frontend/test/lit/GraphDecomposition/TestOnDemandStaticData.mlir b/frontend/test/lit/GraphDecomposition/TestOnDemandStaticData.mlir new file mode 100644 index 0000000000..ab0cc56bc9 --- /dev/null +++ b/frontend/test/lit/GraphDecomposition/TestOnDemandStaticData.mlir @@ -0,0 +1,37 @@ +// 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: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=PhaseShift=1.0,GlobalPhase=1.0})' %s | FileCheck %s + +// PCPhase keeps its `dim` in its static data as an integer attribute, so asking the frontend for its +// rules on demand only works if that attribute reaches Python as an int (see +// getPyvalFromMlirAttribute in PythonFunction.cpp). If it arrives as anything else, the rule fails +// to build, the solver finds nothing for the op, and the pass reports it as undecomposable. + +// CHECK-LABEL: func.func @circuit +// CHECK-NOT: quantum.pcphase +// CHECK: quantum.operator "PhaseShift" +// CHECK: quantum.gphase +func.func @circuit(%theta: f64) -> !quantum.reg { + %r = quantum.alloc(2) : !quantum.reg + %q0 = quantum.extract %r[0] : !quantum.reg -> !quantum.bit + %q1 = quantum.extract %r[1] : !quantum.reg -> !quantum.bit + %out:2 = quantum.pcphase(%theta, dim : 2) %q0, %q1 : !quantum.bit, !quantum.bit + %r0 = quantum.insert %r[0], %out#0 : !quantum.reg, !quantum.bit + %r1 = quantum.insert %r0[1], %out#1 : !quantum.reg, !quantum.bit + return %r1 : !quantum.reg +} + +// The rule came back keyed on the id the compiler prints for the op, `dim` included. +// CHECK: target_gate = "PCPhase{phi:[f64]}{wires:2}{dim = 2 : i64}" diff --git a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp index a115769662..13cf6efa14 100644 --- a/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp +++ b/mlir/lib/Quantum/Transforms/QuantumPythonDecompositions/PythonFunction.cpp @@ -21,6 +21,7 @@ #include #include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" @@ -30,8 +31,10 @@ #include "llvm/Support/raw_ostream.h" #include "mlir/IR/Attributes.h" #include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/TypeRange.h" #include "mlir/IR/Types.h" +#include "mlir/Support/DebugStringHelper.h" #include "Quantum/IR/QuantumInterfaces.h" #include "Quantum/IR/QuantumOps.h" @@ -82,6 +85,35 @@ static nb::object getPyvalFromTypeRange(mlir::TypeRange typerange) { return pyTypes; } +// Read an integer attribute the way MLIR's own printer does, so a value that crosses into the +// frontend and is printed back into a graphOpId keeps the spelling it arrived with: unsigned types +// zero-extend, while signed and signless ones sign-extend (a signless negative prints as `-5:i64`). +static nb::object getPyvalFromIntegerAttribute(mlir::IntegerAttr intAttr) { + llvm::APInt value = intAttr.getValue(); + // An `index` attribute is an IntegerAttr with no signedness of its own; reading it as signed is + // what IntegerAttr::getInt does. + bool isUnsigned = intAttr.getType().isUnsignedInteger(); + + // APInt only surrenders its value 64 bits at a time, so a wider one goes through its decimal + // spelling. Python integers have no width of their own to overflow. + if (isUnsigned ? value.getActiveBits() > 64 : value.getSignificantBits() > 64) { + llvm::SmallString<40> digits; + value.toString(digits, /*Radix=*/10, /*Signed=*/!isUnsigned); + PyObject *pyInt = PyLong_FromString(digits.c_str(), /*pend=*/nullptr, /*base=*/10); + if (!pyInt) { + throw nb::python_error(); + } + return nb::steal(pyInt); + } + + if (isUnsigned) { + return nb::cast(value.getZExtValue()); + } + return nb::cast(value.getSExtValue()); +} + +// Convert an MLIR attribute into an equivalent Python value. Generally should represent the +// inverse of `get_mlir_attribute_from_pyval` from the frontend direction. static nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) { return llvm::TypeSwitch(attr) .Case([](auto dictAttr) { @@ -93,14 +125,23 @@ static nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) { return outDict; }) .Case([](auto arrAttr) { - nb::list outTuple; + nb::list outList; for (auto val : arrAttr) { - outTuple.append(getPyvalFromMlirAttribute(val)); + outList.append(getPyvalFromMlirAttribute(val)); } - return outTuple; + return outList; }) .Case([](auto strAttr) { return nb::cast(strAttr.getValue().str()); }) - .Default([](auto attr) { return nb::str("placeholder"); }); + // Needs to be before IntegerAttr, since bools are also integers (i1). + .Case([](auto boolAttr) { return nb::cast(boolAttr.getValue()); }) + .Case([](auto intAttr) { return getPyvalFromIntegerAttribute(intAttr); }) + .Case( + [](auto floatAttr) { return nb::cast(floatAttr.getValueAsDouble()); }) + .Default([](mlir::Attribute attr) -> nb::object { + throw QuantumPythonDecompositions::QPDError( + "Cannot convert the MLIR attribute " + mlir::debugString(attr) + + " to a Python value for graph decomposition, unknown attribute type."); + }); } } // namespace