[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
This commit is contained in:
Maksim Levental 2026-08-01 12:59:13 -07:00 committed by GitHub
parent 84afdb4771
commit e3bde09850
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 240 additions and 1 deletions

View file

@ -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 differently; 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);

View file

@ -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());
}

View file

@ -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 differ.
assert(mlirOperationIsStructurallyEquivalent(
c42a, c42b, MLIR_OPERATION_EQUIVALENCE_IGNORE_LOCATIONS));
// Without ignoring locations they differ, since they sit on distinct lines.
assert(!mlirOperationIsStructurallyEquivalent(
c42a, c42b, MLIR_OPERATION_EQUIVALENCE_NONE));
// A constant with a different 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 different 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 differ 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 differ 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
// different 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 differ.
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