From e3bde09850d7be0bfdbd9051f09b887b1d607f38 Mon Sep 17 00:00:00 2001 From: Maksim Levental Date: Sat, 1 Aug 2026 12:59:13 -0700 Subject: [PATCH] [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 --- mlir/include/mlir-c/IR.h | 39 ++++++++- mlir/lib/CAPI/IR/IR.cpp | 19 ++++ mlir/test/CAPI/ir.c | 183 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 1 deletion(-) diff --git a/mlir/include/mlir-c/IR.h b/mlir/include/mlir-c/IR.h index ba838043c4b8..ac8ffbaf028b 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 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); diff --git a/mlir/lib/CAPI/IR/IR.cpp b/mlir/lib/CAPI/IR/IR.cpp index bceed8eeea4a..ef730b26cdd5 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(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 f73153bc5412..bd9c8e0f5077 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 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