[Mlir-commits] [mlir] [mlir][emitc] Fix crash in form-expressions when identity cast is folded (PR #183894)

Mehdi Amini llvmlistbot at llvm.org
Sat Feb 28 02:20:32 PST 2026


https://github.com/joker-eph created https://github.com/llvm/llvm-project/pull/183894

When the --form-expressions pass runs applyPatternsGreedily, the greedy rewriter calls CastOpInterface::foldTrait on ops inside ExpressionOp bodies. For an identity cast (e.g. `emitc.cast i32 to i32`), this fold succeeds and replaces the op's result with its operand (a block argument), leaving the ExpressionOp body in the form `{ yield %arg }`.

Subsequently, if the cast expression's result had its use count reduced to one (e.g. because a dead non-EmitC use was eliminated), FoldExpressionOp would select this expression as a candidate to fold into an outer expression. It then calls usedExpression.getRootOp(), which returns nullptr because the body yields a block argument rather than an op result. The subsequent mapper.lookup(nullptr) crashes with an assertion.

Fix by adding a FoldTrivialExpressionOp canonicalization pattern that handles ExpressionOps whose body yields a block argument directly: such an expression is a passthrough and can be replaced by the corresponding operand value. This canonicalization fires during greedy rewriting before FoldExpressionOp encounters the invalid state.

Fixes #179844

>From 6056c149d44627662ccdbcdbd04a77a859e047b1 Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Fri, 27 Feb 2026 13:04:54 -0800
Subject: [PATCH] [mlir][emitc] Fix crash in form-expressions when identity
 cast is folded

When the --form-expressions pass runs applyPatternsGreedily, the greedy
rewriter calls CastOpInterface::foldTrait on ops inside ExpressionOp
bodies. For an identity cast (e.g. `emitc.cast i32 to i32`), this fold
succeeds and replaces the op's result with its operand (a block
argument), leaving the ExpressionOp body in the form `{ yield %arg }`.

Subsequently, if the cast expression's result had its use count reduced
to one (e.g. because a dead non-EmitC use was eliminated), FoldExpressionOp
would select this expression as a candidate to fold into an outer
expression. It then calls usedExpression.getRootOp(), which returns
nullptr because the body yields a block argument rather than an op
result. The subsequent mapper.lookup(nullptr) crashes with an assertion.

Fix by adding a FoldTrivialExpressionOp canonicalization pattern that
handles ExpressionOps whose body yields a block argument directly: such
an expression is a passthrough and can be replaced by the corresponding
operand value. This canonicalization fires during greedy rewriting before
FoldExpressionOp encounters the invalid state.

Fixes #179844
---
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp           | 22 ++++++++++++++-
 mlir/test/Dialect/EmitC/form-expressions.mlir | 28 +++++++++++++++++++
 2 files changed, 49 insertions(+), 1 deletion(-)

diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index fa8a159b50d98..6979f34c1e047 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -461,11 +461,31 @@ struct RemoveRecurringExpressionOperands
   }
 };
 
+/// If an ExpressionOp body yields a block argument directly (no root op),
+/// this means a contained op was folded away (e.g., an identity cast whose
+/// in/out types match). Canonicalize by replacing the expression with the
+/// corresponding operand value.
+struct FoldTrivialExpressionOp : public OpRewritePattern<ExpressionOp> {
+  using OpRewritePattern<ExpressionOp>::OpRewritePattern;
+  LogicalResult matchAndRewrite(ExpressionOp expressionOp,
+                                PatternRewriter &rewriter) const override {
+    auto yieldOp = cast<YieldOp>(expressionOp.getBody()->getTerminator());
+    Value yieldedValue = yieldOp.getResult();
+    auto blockArg = dyn_cast_if_present<BlockArgument>(yieldedValue);
+    if (!blockArg)
+      return failure();
+    rewriter.replaceOp(expressionOp,
+                       expressionOp.getOperand(blockArg.getArgNumber()));
+    return success();
+  }
+};
+
 } // namespace
 
 void ExpressionOp::getCanonicalizationPatterns(RewritePatternSet &results,
                                                MLIRContext *context) {
-  results.add<RemoveRecurringExpressionOperands>(context);
+  results.add<RemoveRecurringExpressionOperands, FoldTrivialExpressionOp>(
+      context);
 }
 
 ParseResult ExpressionOp::parse(OpAsmParser &parser, OperationState &result) {
diff --git a/mlir/test/Dialect/EmitC/form-expressions.mlir b/mlir/test/Dialect/EmitC/form-expressions.mlir
index 58eac4381ccb7..f5c002bba84a1 100644
--- a/mlir/test/Dialect/EmitC/form-expressions.mlir
+++ b/mlir/test/Dialect/EmitC/form-expressions.mlir
@@ -227,3 +227,31 @@ func.func @expression_with_constant(%arg0: i32) -> i32 {
   %a = emitc.mul %arg0, %c42 : (i32, i32) -> i32
   return %a : i32
 }
+
+// Regression test for https://github.com/llvm/llvm-project/issues/179844:
+// An identity cast (same in/out type) inside an expression is folded away
+// by CastOpInterface::foldTrait, leaving the expression body yielding a block
+// argument. FoldTrivialExpressionOp must canonicalize such expressions before
+// FoldExpressionOp tries to fold them.
+//
+// CHECK-LABEL: func.func @identity_cast_folded(
+// CHECK-SAME:                                  %[[ARG0:.*]]: i32) -> i32 {
+// CHECK:         %[[EXPR:.*]] = emitc.expression %[[ARG0]] : (i32) -> i32 {
+// CHECK:           %[[RES:.*]] = bitwise_and %[[ARG0]], %[[ARG0]] : (i32, i32) -> i32
+// CHECK:           yield %[[RES]] : i32
+// CHECK:         }
+// CHECK:         return %[[EXPR]] : i32
+// CHECK:       }
+
+func.func @identity_cast_folded(%arg0: i32) -> i32 {
+  // emitc.cast i32->i32 is an identity cast; CastOpInterface folds it away.
+  // After folding, FoldTrivialExpressionOp must canonicalize the expression
+  // that wraps this cast (which just yields its block arg) before
+  // FoldExpressionOp tries to fold it into the bitwise_and expression.
+  %0 = emitc.cast %arg0 : i32 to i32
+  // The bitwise_and uses both the cast result and the original arg, giving
+  // two uses of the cast expression's result (one from this op, one from the
+  // identity comparison below that keeps the cast live during canonicalization).
+  %1 = emitc.bitwise_and %0, %arg0 : (i32, i32) -> i32
+  return %1 : i32
+}



More information about the Mlir-commits mailing list