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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions frontend/test/lit/GraphDecomposition/TestOnDemandStaticData.mlir
Original file line number Diff line number Diff line change
@@ -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}"
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <string>

#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"
Expand All @@ -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"
Expand Down Expand Up @@ -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<mlir::Attribute, nb::object>(attr)
.Case<mlir::DictionaryAttr>([](auto dictAttr) {
Expand All @@ -93,14 +125,23 @@ static nb::object getPyvalFromMlirAttribute(mlir::Attribute attr) {
return outDict;
})
.Case<mlir::ArrayAttr>([](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<mlir::StringAttr>([](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<mlir::BoolAttr>([](auto boolAttr) { return nb::cast(boolAttr.getValue()); })
.Case<mlir::IntegerAttr>([](auto intAttr) { return getPyvalFromIntegerAttribute(intAttr); })
.Case<mlir::FloatAttr>(
[](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
Expand Down
Loading