[Mlir-commits] [mlir] e3bde09 - [mlir-c] Add structural operation equivalence (#206537)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Sat Aug 1 12:59:18 PDT 2026
Author: Maksim Levental
Date: 2026-08-01T12:59:13-07:00
New Revision: e3bde09850d7be0bfdbd9051f09b887b1d607f38
URL: https://github.com/llvm/llvm-project/commit/e3bde09850d7be0bfdbd9051f09b887b1d607f38
DIFF: https://github.com/llvm/llvm-project/commit/e3bde09850d7be0bfdbd9051f09b887b1d607f38.diff
LOG: [mlir-c] Add structural operation equivalence (#206537)
Exposes `OperationEquivalence` through the MLIR C API so callers can compare operations structurally rather than by handle identity.
Assisted by: Claude
Added:
Modified:
mlir/include/mlir-c/IR.h
mlir/lib/CAPI/IR/IR.cpp
mlir/test/CAPI/ir.c
Removed:
################################################################################
diff --git a/mlir/include/mlir-c/IR.h b/mlir/include/mlir-c/IR.h
index ba838043c4b87..ac8ffbaf028b0 100644
--- a/mlir/include/mlir-c/IR.h
+++ b/mlir/include/mlir-c/IR.h
@@ -639,9 +639,46 @@ static inline bool mlirOperationIsNull(MlirOperation op) { return !op.ptr; }
MLIR_CAPI_EXPORTED bool mlirOperationEqual(MlirOperation op,
MlirOperation other);
-/// Compute a hash for the given operation.
+/// Compute a hash for the given operation. Operand and result SSA values are
+/// hashed by identity and locations are significant, so equivalent-but-distinct
+/// operations hash
diff erently; use mlirOperationStructuralHashValue for a hash
+/// that pairs with mlirOperationIsStructurallyEquivalent.
MLIR_CAPI_EXPORTED size_t mlirOperationHashValue(MlirOperation op);
+/// Flags controlling structural operation equivalence and hashing. These mirror
+/// `mlir::OperationEquivalence::Flags` and may be combined with bitwise OR.
+typedef enum MlirOperationEquivalenceFlags {
+ /// No flags: locations, discardable attributes, properties and
+ /// commutativity are all significant.
+ MLIR_OPERATION_EQUIVALENCE_NONE = 0,
+ /// Ignore the locations attached to operations.
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS = 1,
+ /// Ignore the discardable attributes attached to operations.
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_DISCARDABLE_ATTRS = 2,
+ /// Ignore the properties attached to operations.
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_PROPERTIES = 4,
+ /// Ignore commutativity, comparing operands in an order-sensitive way.
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_COMMUTATIVITY = 8,
+} MlirOperationEquivalenceFlags;
+
+/// Checks whether two operations are structurally equivalent, i.e. they have
+/// the same name, attributes, operand and result types, and recursively
+/// equivalent regions. Operand equivalence is tracked structurally while
+/// recursing into regions, so operands defined inside the compared regions need
+/// not be the exact same SSA values; operands defined outside must be. `flags`
+/// is a bitwise OR of MlirOperationEquivalenceFlags values.
+MLIR_CAPI_EXPORTED bool mlirOperationIsStructurallyEquivalent(MlirOperation lhs,
+ MlirOperation rhs,
+ uint32_t flags);
+
+/// Computes a hash for the given operation that pairs with
+/// mlirOperationIsStructurallyEquivalent: two operations that are structurally
+/// equivalent under the same `flags` hash equally. Operands are hashed by
+/// identity, results are not hashed at all, and regions do not participate in
+/// the hash. `flags` is a bitwise OR of MlirOperationEquivalenceFlags values.
+MLIR_CAPI_EXPORTED size_t mlirOperationStructuralHashValue(MlirOperation op,
+ uint32_t flags);
+
/// Gets the context this operation is associated with
MLIR_CAPI_EXPORTED MlirContext mlirOperationGetContext(MlirOperation op);
diff --git a/mlir/lib/CAPI/IR/IR.cpp b/mlir/lib/CAPI/IR/IR.cpp
index bceed8eeea4a9..ef730b26cdd5d 100644
--- a/mlir/lib/CAPI/IR/IR.cpp
+++ b/mlir/lib/CAPI/IR/IR.cpp
@@ -661,6 +661,25 @@ size_t mlirOperationHashValue(MlirOperation op) {
return OperationEquivalence::computeHash(unwrap(op));
}
+/// Translates the C equivalence flags to mlir::OperationEquivalence::Flags. The
+/// enumerator values mirror each other.
+static OperationEquivalence::Flags unwrapEquivalenceFlags(uint32_t flags) {
+ return static_cast<OperationEquivalence::Flags>(flags);
+}
+
+bool mlirOperationIsStructurallyEquivalent(MlirOperation lhs, MlirOperation rhs,
+ uint32_t flags) {
+ return OperationEquivalence::isEquivalentTo(unwrap(lhs), unwrap(rhs),
+ unwrapEquivalenceFlags(flags));
+}
+
+size_t mlirOperationStructuralHashValue(MlirOperation op, uint32_t flags) {
+ return OperationEquivalence::computeHash(
+ unwrap(op), /*hashOperands=*/OperationEquivalence::directHashValue,
+ /*hashResults=*/OperationEquivalence::ignoreHashValue,
+ unwrapEquivalenceFlags(flags));
+}
+
MlirContext mlirOperationGetContext(MlirOperation op) {
return wrap(unwrap(op)->getContext());
}
diff --git a/mlir/test/CAPI/ir.c b/mlir/test/CAPI/ir.c
index f73153bc54129..bd9c8e0f50778 100644
--- a/mlir/test/CAPI/ir.c
+++ b/mlir/test/CAPI/ir.c
@@ -3034,6 +3034,187 @@ int testReplaceUsesWithIf(MlirContext ctx) {
return 0;
}
+int testOperationEquivalence(MlirContext ctx) {
+ fprintf(stderr, "@testOperationEquivalence\n");
+ // CHECK-LABEL: @testOperationEquivalence
+
+ mlirContextGetOrLoadDialect(ctx, mlirStringRefCreateFromCString("arith"));
+
+ const char *moduleStr = "func.func @f(%arg0: i32) -> i32 {\n"
+ " %0 = arith.constant 42 : i32\n"
+ " %1 = arith.constant 42 : i32\n"
+ " %2 = arith.constant 7 : i32\n"
+ " %3 = arith.subi %0, %2 : i32\n"
+ " %4 = arith.subi %0, %2 : i32\n"
+ " %5 = arith.subi %2, %0 : i32\n"
+ " %6 = arith.subi %0, %2 {dialect.discardable} : "
+ "i32\n"
+ " %7 = arith.addi %0, %2 : i32\n"
+ " %8 = arith.addi %2, %0 : i32\n"
+ " return %0 : i32\n"
+ "}\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleStr));
+
+ MlirBlock moduleBody = mlirModuleGetBody(module);
+ MlirOperation funcOp = mlirBlockGetFirstOperation(moduleBody);
+ MlirRegion funcRegion = mlirOperationGetRegion(funcOp, 0);
+ MlirBlock funcBody = mlirRegionGetFirstBlock(funcRegion);
+
+ MlirOperation c42a = mlirBlockGetFirstOperation(funcBody);
+ MlirOperation c42b = mlirOperationGetNextInBlock(c42a);
+ MlirOperation c7 = mlirOperationGetNextInBlock(c42b);
+ MlirOperation sub3 = mlirOperationGetNextInBlock(c7);
+ MlirOperation sub4 = mlirOperationGetNextInBlock(sub3);
+ MlirOperation sub5 = mlirOperationGetNextInBlock(sub4);
+ MlirOperation sub6 = mlirOperationGetNextInBlock(sub5);
+ MlirOperation add7 = mlirOperationGetNextInBlock(sub6);
+ MlirOperation add8 = mlirOperationGetNextInBlock(add7);
+
+ // Two identical constants are structurally equivalent when locations are
+ // ignored, even though their result SSA values and source locations
diff er.
+ assert(mlirOperationIsStructurallyEquivalent(
+ c42a, c42b, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // Without ignoring locations they
diff er, since they sit on distinct lines.
+ assert(!mlirOperationIsStructurallyEquivalent(
+ c42a, c42b, MLIR_OPERATION_EQUIVALENCE_NONE));
+
+ // A constant with a
diff erent value is not equivalent.
+ assert(!mlirOperationIsStructurallyEquivalent(
+ c42a, c7, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // Equivalence is reflexive.
+ assert(mlirOperationIsStructurallyEquivalent(
+ c42a, c42a, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // `%3 = subi %0, %2` and `%4 = subi %0, %2` use the exact same operands, so
+ // they are equivalent.
+ assert(mlirOperationIsStructurallyEquivalent(
+ sub3, sub4, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // `%5 = subi %2, %0` is not equivalent to `%3 = subi %0, %2`: `subi` is not
+ // commutative, so operands are compared pairwise, and the very first pair
+ // (`%0` = 42 vs `%2` = 7) already mismatches. See the `addi` pair below for
+ // the commutative path.
+ assert(!mlirOperationIsStructurallyEquivalent(
+ sub3, sub5, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // A constant and a subtraction are
diff erent operations.
+ assert(!mlirOperationIsStructurallyEquivalent(
+ c42a, sub3, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // `%6 = subi %0, %2 {dialect.discardable}` carries a discardable attribute
+ // that `%3 = subi %0, %2` does not, so the two
diff er unless discardable
+ // attributes are ignored as well. This also exercises OR-ing flags together.
+ assert(!mlirOperationIsStructurallyEquivalent(
+ sub3, sub6, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+ assert(mlirOperationIsStructurallyEquivalent(
+ sub3, sub6,
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS |
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_DISCARDABLE_ATTRS));
+
+ // `%7 = addi %0, %2` and `%8 = addi %2, %0` have swapped operands, but `addi`
+ // is commutative, so they are equivalent through the commutative path and
+ // only
diff er once commutativity is explicitly ignored.
+ assert(mlirOperationIsStructurallyEquivalent(
+ add7, add8, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+ assert(!mlirOperationIsStructurallyEquivalent(
+ add7, add8,
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS |
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_COMMUTATIVITY));
+
+ // The `value` of arith.constant is an inherent attribute held in the
+ // operation's properties, so ignoring properties makes constants with
+ //
diff erent values equivalent.
+ assert(mlirOperationIsStructurallyEquivalent(
+ c42a, c7,
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS |
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_PROPERTIES));
+
+ // The structural hash pairs with the equivalence above: operations that are
+ // equivalent under a set of flags hash equally under the same flags.
+ assert(mlirOperationStructuralHashValue(
+ c42a, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS) ==
+ mlirOperationStructuralHashValue(
+ c42b, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+ assert(mlirOperationStructuralHashValue(
+ sub3, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS) ==
+ mlirOperationStructuralHashValue(
+ sub4, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+ assert(mlirOperationStructuralHashValue(
+ sub3, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS |
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_DISCARDABLE_ATTRS) ==
+ mlirOperationStructuralHashValue(
+ sub6, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS |
+ MLIR_OPERATION_EQUIVALENCE_IGNORE_DISCARDABLE_ATTRS));
+ // Commutative operands are folded into the hash in an order-insensitive way,
+ // matching the commutative equivalence above.
+ assert(mlirOperationStructuralHashValue(
+ add7, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS) ==
+ mlirOperationStructuralHashValue(
+ add8, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // The flags are threaded through to the hash: locations and discardable
+ // attributes are hashed unless the corresponding flag is set. Hash
+ // inequality is not part of the contract, but these inputs do
diff er.
+ assert(
+ mlirOperationStructuralHashValue(c42a, MLIR_OPERATION_EQUIVALENCE_NONE) !=
+ mlirOperationStructuralHashValue(c42b, MLIR_OPERATION_EQUIVALENCE_NONE));
+ assert(mlirOperationStructuralHashValue(
+ sub3, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS) !=
+ mlirOperationStructuralHashValue(
+ sub6, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ mlirModuleDestroy(module);
+
+ // Operands defined *inside* the compared regions need not be the same SSA
+ // values: recursing into the regions marks the two inner constants
+ // equivalent, and the `addi` operands are then matched through that mapping.
+ const char *regionModuleStr = "func.func @g() {\n"
+ " %r0 = scf.execute_region -> i32 {\n"
+ " %c = arith.constant 1 : i32\n"
+ " %d = arith.addi %c, %c : i32\n"
+ " scf.yield %d : i32\n"
+ " }\n"
+ " %r1 = scf.execute_region -> i32 {\n"
+ " %c = arith.constant 1 : i32\n"
+ " %d = arith.addi %c, %c : i32\n"
+ " scf.yield %d : i32\n"
+ " }\n"
+ " return\n"
+ "}\n";
+ MlirModule regionModule = mlirModuleCreateParse(
+ ctx, mlirStringRefCreateFromCString(regionModuleStr));
+ assert(!mlirModuleIsNull(regionModule));
+ MlirBlock regionFuncBody = mlirRegionGetFirstBlock(mlirOperationGetRegion(
+ mlirBlockGetFirstOperation(mlirModuleGetBody(regionModule)), 0));
+ MlirOperation exec0 = mlirBlockGetFirstOperation(regionFuncBody);
+ MlirOperation exec1 = mlirOperationGetNextInBlock(exec0);
+ assert(mlirOperationIsStructurallyEquivalent(
+ exec0, exec1, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ // The nested `addi` operations compared on their own are *not* equivalent:
+ // the value mapping starts out empty, so operands defined outside the
+ // compared operation must be the exact same SSA values.
+ MlirBlock exec0Body =
+ mlirRegionGetFirstBlock(mlirOperationGetRegion(exec0, 0));
+ MlirBlock exec1Body =
+ mlirRegionGetFirstBlock(mlirOperationGetRegion(exec1, 0));
+ MlirOperation innerAdd0 =
+ mlirOperationGetNextInBlock(mlirBlockGetFirstOperation(exec0Body));
+ MlirOperation innerAdd1 =
+ mlirOperationGetNextInBlock(mlirBlockGetFirstOperation(exec1Body));
+ assert(!mlirOperationIsStructurallyEquivalent(
+ innerAdd0, innerAdd1, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
+
+ mlirModuleDestroy(regionModule);
+
+ // CHECK: testOperationEquivalence: PASSED
+ fprintf(stderr, "testOperationEquivalence: PASSED\n");
+ return 0;
+}
+
int main(void) {
MlirContext ctx = mlirContextCreate();
registerAllUpstreamDialects(ctx);
@@ -3091,6 +3272,8 @@ int main(void) {
return 20;
if (testReplaceUsesWithIf(ctx))
return 21;
+ if (testOperationEquivalence(ctx))
+ return 22;
// CHECK: DESTROY MAIN CONTEXT
// CHECK: reportResourceDelete: resource_i64_blob
More information about the Mlir-commits
mailing list