[Mlir-commits] [mlir] Add Python bindings for operation structural equivalence and hashing (PR #216357)
Trevor McKay
llvmlistbot at llvm.org
Fri Aug 14 09:57:50 PDT 2026
https://github.com/trmckay created https://github.com/llvm/llvm-project/pull/216357
Adds Python bindings for `is_structurally_equivalent` and `structural_hash` and the flags which they both take as arguments. The C APIs already exist, so this just exposes them in Python.
>From a01b7a77964ef7f2367d8173250287c35701cdc3 Mon Sep 17 00:00:00 2001
From: Trevor McKay <mail at tmckay.com>
Date: Thu, 13 Aug 2026 14:48:15 -0700
Subject: [PATCH] add Python bindings for structural equivalence and hash
---
mlir/include/mlir/Bindings/Python/IRCore.h | 10 +++
mlir/lib/Bindings/Python/IRCore.cpp | 39 ++++++++++++
mlir/test/python/ir/operation.py | 71 ++++++++++++++++++++++
3 files changed, 120 insertions(+)
diff --git a/mlir/include/mlir/Bindings/Python/IRCore.h b/mlir/include/mlir/Bindings/Python/IRCore.h
index 3314e0b2a8fcf..05c7d63ccf766 100644
--- a/mlir/include/mlir/Bindings/Python/IRCore.h
+++ b/mlir/include/mlir/Bindings/Python/IRCore.h
@@ -365,6 +365,16 @@ enum class PyWalkOrder : std::underlying_type_t<MlirWalkOrder> {
PostOrder = MlirWalkPostOrder
};
+/// Flags controlling structural operation equivalence and hashing.
+enum class PyOperationEquivalenceFlags : std::underlying_type_t<
+ MlirOperationEquivalenceFlags> {
+ None = MLIR_OPERATION_EQUIVALENCE_NONE,
+ IgnoreLocations = MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS,
+ IgnoreDiscardableAttrs = MLIR_OPERATION_EQUIVALENCE_IGNORE_DISCARDABLE_ATTRS,
+ IgnoreProperties = MLIR_OPERATION_EQUIVALENCE_IGNORE_PROPERTIES,
+ IgnoreCommutativity = MLIR_OPERATION_EQUIVALENCE_IGNORE_COMMUTATIVITY
+};
+
/// Python class mirroring the C MlirDiagnostic struct. Note that these structs
/// are only valid for the duration of a diagnostic callback and attempting
/// to access them outside of that will raise an exception. This applies to
diff --git a/mlir/lib/Bindings/Python/IRCore.cpp b/mlir/lib/Bindings/Python/IRCore.cpp
index 75cfd2a0a1c0b..95eb3d857db41 100644
--- a/mlir/lib/Bindings/Python/IRCore.cpp
+++ b/mlir/lib/Bindings/Python/IRCore.cpp
@@ -3291,6 +3291,17 @@ void populateIRCore(nb::module_ &m) {
nb::enum_<PyWalkOrder>(m, "WalkOrder")
.value("PRE_ORDER", PyWalkOrder::PreOrder)
.value("POST_ORDER", PyWalkOrder::PostOrder);
+
+ nb::enum_<PyOperationEquivalenceFlags>(m, "OperationEquivalenceFlags",
+ nb::is_arithmetic(), nb::is_flag())
+ .value("NONE", PyOperationEquivalenceFlags::None)
+ .value("IGNORE_LOCATIONS", PyOperationEquivalenceFlags::IgnoreLocations)
+ .value("IGNORE_DISCARDABLE_ATTRS",
+ PyOperationEquivalenceFlags::IgnoreDiscardableAttrs)
+ .value("IGNORE_PROPERTIES", PyOperationEquivalenceFlags::IgnoreProperties)
+ .value("IGNORE_COMMUTATIVITY",
+ PyOperationEquivalenceFlags::IgnoreCommutativity);
+
nb::enum_<PyWalkResult>(m, "WalkResult")
.value("ADVANCE", PyWalkResult::Advance)
.value("INTERRUPT", PyWalkResult::Interrupt)
@@ -3946,6 +3957,34 @@ void populateIRCore(nb::module_ &m) {
return mlirOperationHashValue(self.getOperation().get());
},
"Returns the hash value of the operation.")
+ .def(
+ "is_structurally_equivalent",
+ [](PyOperationBase &self, PyOperationBase &other,
+ PyOperationEquivalenceFlags flags) {
+ self.getOperation().checkValid();
+ other.getOperation().checkValid();
+ return mlirOperationIsStructurallyEquivalent(
+ self.getOperation().get(), other.getOperation().get(),
+ static_cast<uint32_t>(flags));
+ },
+ "other"_a, "flags"_a = PyOperationEquivalenceFlags::None,
+ R"(Checks whether two operations are structurally equivalent.
+
+The IGNORE_LOCATIONS, IGNORE_DISCARDABLE_ATTRS, IGNORE_PROPERTIES, and
+IGNORE_COMMUTATIVITY flags ignore locations, discardable attributes, properties,
+and commutativity, respectively. The predicate recursively compares regions.)")
+ .def(
+ "structural_hash",
+ [](PyOperationBase &self, PyOperationEquivalenceFlags flags) {
+ self.getOperation().checkValid();
+ return mlirOperationStructuralHashValue(
+ self.getOperation().get(), static_cast<uint32_t>(flags));
+ },
+ "flags"_a = PyOperationEquivalenceFlags::None,
+ R"(Computes a structural hash for the operation.
+
+Use the same flags as is_structurally_equivalent. The hash does not recurse into
+regions, unlike the predicate.)")
.def_prop_ro(
"attributes",
[](PyOperationBase &self) {
diff --git a/mlir/test/python/ir/operation.py b/mlir/test/python/ir/operation.py
index 9f1ad01cbcc88..e061d4711c21c 100644
--- a/mlir/test/python/ir/operation.py
+++ b/mlir/test/python/ir/operation.py
@@ -1231,6 +1231,77 @@ def testOperationHash():
assert hash(op2) == hash(custom_op2)
+# CHECK-LABEL: TEST: testOperationEquivalence
+ at run
+def testOperationEquivalence():
+ ctx = Context()
+ with ctx:
+ module = Module.parse(
+ r"""
+ func.func @f() -> i32 {
+ %0 = arith.constant 42 : i32
+ %1 = arith.constant 42 : i32
+ %2 = arith.constant 7 : i32
+ %3 = arith.subi %0, %2 : i32
+ %4 = arith.subi %0, %2 : i32
+ %5 = arith.subi %2, %0 : i32
+ %6 = arith.subi %0, %2 {dialect.discardable} : i32
+ %7 = arith.addi %0, %2 : i32
+ %8 = arith.addi %2, %0 : i32
+ return %0 : i32
+ }
+ """,
+ ctx,
+ )
+ operations = list(module.body.operations[0].regions[0].blocks[0].operations)
+ c42a, c42b, c7, sub3, sub4, sub5, sub6, add7, add8 = operations[:-1]
+ flags = OperationEquivalenceFlags
+ ignore_locations = flags.IGNORE_LOCATIONS
+
+ assert c42a.is_structurally_equivalent(c42b, ignore_locations)
+ assert not c42a.is_structurally_equivalent(c42b)
+ assert c42a.is_structurally_equivalent(c42b) == c42a.is_structurally_equivalent(
+ c42b, flags.NONE
+ )
+ assert not c42a.is_structurally_equivalent(c7, ignore_locations)
+ assert c42a.is_structurally_equivalent(c42a, ignore_locations)
+ assert sub3.is_structurally_equivalent(sub4, ignore_locations)
+ assert not sub3.is_structurally_equivalent(sub5, ignore_locations)
+ assert not c42a.is_structurally_equivalent(sub3, ignore_locations)
+
+ discardable_flags = (
+ ignore_locations | flags.IGNORE_DISCARDABLE_ATTRS
+ )
+ assert not sub3.is_structurally_equivalent(sub6, ignore_locations)
+ assert sub3.is_structurally_equivalent(sub6, discardable_flags)
+
+ assert add7.is_structurally_equivalent(add8, ignore_locations)
+ assert not add7.is_structurally_equivalent(
+ add8, ignore_locations | flags.IGNORE_COMMUTATIVITY
+ )
+
+ properties_flags = ignore_locations | flags.IGNORE_PROPERTIES
+ assert c42a.is_structurally_equivalent(c7, properties_flags)
+
+ assert c42a.structural_hash(ignore_locations) == c42b.structural_hash(
+ ignore_locations
+ )
+ assert sub3.structural_hash(ignore_locations) == sub4.structural_hash(
+ ignore_locations
+ )
+ assert sub3.structural_hash(discardable_flags) == sub6.structural_hash(
+ discardable_flags
+ )
+ assert add7.structural_hash(ignore_locations) == add8.structural_hash(
+ ignore_locations
+ )
+ assert c42a.structural_hash(properties_flags) == c7.structural_hash(
+ properties_flags
+ )
+ # CHECK: operation equivalence passed
+ print("operation equivalence passed")
+
+
# CHECK-LABEL: TEST: testOperationParse
@run
def testOperationParse():
More information about the Mlir-commits
mailing list