[Flow] Fold static flow.tensor.empty shapes into dims (#24622)

This PR Adds a canonicalization pattern for `flow.tensor.empty` that
folds constant dynamic dimension operands into the static result shape.

**Example :**

%0 = flow.tensor.empty : tensor<?x4xf32>{%c8}

**canonicalizes to:**

%0 = flow.tensor.empty : tensor<8x4xf32>
%cast = tensor.cast %0 : tensor<8x4xf32> to tensor<?x4xf32>

The cast preserves the original result type for existing users while
allowing downstream canonicalization to use the refined static shape.

---------

Signed-off-by: LekkalaSravya3 <lekkala.sravya@multicorewareinc.com>
diff --git a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp
index 76b0315..9e46d78 100644
--- a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp
+++ b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp
@@ -743,9 +743,62 @@
 // flow.tensor.empty
 //===----------------------------------------------------------------------===//
 
+namespace {
+
+// Folds constant dynamic dimension operands of a flow.tensor.empty into the
+// static result shape, e.g. `flow.tensor.empty : tensor<?xi32>{%c4}` becomes
+// `flow.tensor.empty : tensor<4xi32>`. A cast is inserted to preserve the
+// original (dynamic) result type for existing users; it folds away once
+// those users can accept the refined static type.
+struct FoldTensorEmptyConstantDims : public OpRewritePattern<TensorEmptyOp> {
+  using Base::Base;
+  LogicalResult matchAndRewrite(TensorEmptyOp op,
+                                PatternRewriter &rewriter) const override {
+    auto resultType = cast<RankedTensorType>(op.getResult().getType());
+    ValueRange dynamicDims = op.getResultDims();
+
+    SmallVector<int64_t> newShape(resultType.getShape());
+    // Collect only dimensions that remain dynamic after folding constants.
+    SmallVector<Value> newDynamicDims;
+    unsigned dimIdx = 0;
+    bool didFold = false;
+    for (unsigned i = 0, e = resultType.getRank(); i < e; ++i) {
+      // Skip dimensions that are already static.
+      if (!resultType.isDynamicDim(i)) {
+        continue;
+      }
+      // Get the SSA operand corresponding to dynamic dimension.
+      Value dim = dynamicDims[dimIdx++];
+      std::optional<int64_t> constantDim = getConstantIntValue(dim);
+      if (constantDim && *constantDim >= 0) {
+        // Fold the constant value into the tensor's static shape.
+        newShape[i] = *constantDim;
+        didFold = true;
+      } else {
+        newDynamicDims.push_back(
+            dim); // This dim stays dynamic, so preserve its SSA operand.
+      }
+    }
+    if (!didFold) {
+      return rewriter.notifyMatchFailure(op, "no constant dynamic dims");
+    }
+
+    auto newType = RankedTensorType::get(newShape, resultType.getElementType(),
+                                         resultType.getEncoding());
+    Value newEmpty =
+        TensorEmptyOp::create(rewriter, op.getLoc(), newType, newDynamicDims);
+    // Preserve the original (dynamic) result type for existing users via a
+    // cast; it folds away once those users accept the refined static type.
+    rewriter.replaceOpWithNewOp<tensor::CastOp>(op, resultType, newEmpty);
+    return success();
+  }
+};
+
+} // namespace
+
 void TensorEmptyOp::getCanonicalizationPatterns(RewritePatternSet &results,
                                                 MLIRContext *context) {
-  // TODO(benvanik): fold static shapes into dims.
+  results.insert<FoldTensorEmptyConstantDims>(context);
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/compiler/src/iree/compiler/Dialect/Flow/IR/test/tensor_folding.mlir b/compiler/src/iree/compiler/Dialect/Flow/IR/test/tensor_folding.mlir
index a04dea6..10a470b 100644
--- a/compiler/src/iree/compiler/Dialect/Flow/IR/test/tensor_folding.mlir
+++ b/compiler/src/iree/compiler/Dialect/Flow/IR/test/tensor_folding.mlir
@@ -593,11 +593,55 @@
 util.func public @sliceFromZeroElements(%arg0: tensor<0xi32>) -> tensor<?xi32> {
   %c0 = arith.constant 0 : index
   // CHECK-NOT: flow.tensor.slice
-  // CHECK: %[[RET:.+]] = flow.tensor.empty : tensor<?xi32>{%c0}
+  // CHECK: %[[RET:.+]] = flow.tensor.empty : tensor<0xi32>
+  // CHECK: %[[CAST:.+]] = tensor.cast %[[RET]] : tensor<0xi32> to tensor<?xi32>
   %0 = flow.tensor.slice %arg0[%c0 for %c0] : tensor<0xi32> -> tensor<?xi32>{%c0}
-  // CHECK: util.return %[[RET]]
+  // CHECK: util.return %[[CAST]]
   util.return %0 : tensor<?xi32>
 }
+// -----
+
+// Folds a constant dynamic dimension operand into the static result shape.
+
+// CHECK-LABEL: @emptyFoldConstantDim
+util.func public @emptyFoldConstantDim() -> tensor<?x4xf32> {
+  %c8 = arith.constant 8 : index
+  // CHECK: %[[EMPTY:.+]] = flow.tensor.empty : tensor<8x4xf32>
+  // CHECK: %[[CAST:.+]] = tensor.cast %[[EMPTY]] : tensor<8x4xf32> to tensor<?x4xf32>
+  %0 = flow.tensor.empty : tensor<?x4xf32>{%c8}
+  // CHECK: util.return %[[CAST]]
+  util.return %0 : tensor<?x4xf32>
+}
+
+
+// -----
+
+// Folds only the constant dynamic dimension; the non-constant one stays dynamic.
+
+// CHECK-LABEL: @emptyFoldMixedDims
+// CHECK-SAME: (%[[DIM:.+]]: index)
+util.func public @emptyFoldMixedDims(%dim: index) -> tensor<?x8x?xf32> {
+  %c4 = arith.constant 4 : index
+  // CHECK: %[[EMPTY:.+]] = flow.tensor.empty : tensor<?x8x4xf32>{%[[DIM]]}
+  // CHECK: %[[CAST:.+]] = tensor.cast %[[EMPTY]] : tensor<?x8x4xf32> to tensor<?x8x?xf32>
+  %0 = flow.tensor.empty : tensor<?x8x?xf32>{%dim, %c4}
+  // CHECK: util.return %[[CAST]]
+  util.return %0 : tensor<?x8x?xf32>
+}
+
+// -----
+
+// Leaves non-constant dynamic dimensions untouched.
+
+// CHECK-LABEL: @emptyKeepDynamicDim
+// CHECK-SAME: (%[[DIM:.+]]: index)
+util.func public @emptyKeepDynamicDim(%dim: index) -> tensor<?x4xf32> {
+  // CHECK: %[[RET:.+]] = flow.tensor.empty : tensor<?x4xf32>{%[[DIM]]}
+  %0 = flow.tensor.empty : tensor<?x4xf32>{%dim}
+  // CHECK: util.return %[[RET]]
+  util.return %0 : tensor<?x4xf32>
+}
+
 
 // -----