[Mlir-commits] [mlir] [mlir][mem2reg] Promote memory slots through transparent view operations (PR #196924)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Wed May 13 04:27:18 PDT 2026


https://github.com/jeanPerier updated https://github.com/llvm/llvm-project/pull/196924

>From 18f894775040e4eb61bdcc1a413578e1f080122c Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Mon, 11 May 2026 03:00:52 -0700
Subject: [PATCH 1/5] [mlir][mem2reg] Promote memory slots through transparent
 view operations

---
 .../mlir/Interfaces/MemorySlotInterfaces.h    |  43 +++++++
 .../mlir/Interfaces/MemorySlotInterfaces.td   |  38 ++++++
 mlir/lib/Interfaces/MemorySlotInterfaces.cpp  | 109 ++++++++++++++++++
 mlir/lib/Transforms/Mem2Reg.cpp               |  63 +++++++---
 mlir/test/Transforms/mem2reg.mlir             |  91 +++++++++++++++
 mlir/test/lib/Dialect/Test/TestOpDefs.cpp     |  56 +++++++++
 mlir/test/lib/Dialect/Test/TestOps.td         |  26 +++++
 7 files changed, 413 insertions(+), 13 deletions(-)

diff --git a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
index 7bebfc9a30064..2163593ef823e 100644
--- a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
+++ b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
@@ -30,6 +30,15 @@ struct DestructurableMemorySlot : public MemorySlot {
   DenseMap<Attribute, Type> subelementTypes;
 };
 
+/// Description of a memory slot view produced by a `PromotableOpInterface`
+/// operation: `slotPointerOperand` is the operand viewed by the op,
+/// `view.ptr` is the result aliasing it, and `view.elemType` is the type
+/// at which `view.ptr` aliases the underlying slot.
+struct PromotableSlotView {
+  Value slotPointerOperand;
+  MemorySlot view;
+};
+
 /// Returned by operation promotion logic requesting the deletion of an
 /// operation.
 enum class DeletionKind {
@@ -44,4 +53,38 @@ enum class DeletionKind {
 #include "mlir/Interfaces/MemorySlotOpInterfaces.h.inc"
 #include "mlir/Interfaces/MemorySlotTypeInterfaces.h.inc"
 
+namespace mlir {
+
+/// Returns true if `value` is `rootSlot.ptr` or a transitive view of it,
+/// following `PromotableOpInterface::getPromotableSlotView` chains. The
+/// element type at which `value` aliases the slot is written to
+/// `*outViewElemType` (equal to `rootSlot.elemType` when the chain is empty).
+bool isPromotableSlotView(Value value, const MemorySlot &rootSlot,
+                          Type *outViewElemType = nullptr);
+
+/// Returns a MemorySlot whose `ptr` is the operand of `op` that is a
+/// (possibly transitive) view of `rootSlot.ptr`, with `elemType` equal to
+/// the type at which that operand aliases the slot. Mem2Reg uses this to
+/// hand each `PromotableMemOpInterface` op a slot description tailored to
+/// its memref operand. Returns `nullopt` if no operand is a view of
+/// `rootSlot`.
+std::optional<MemorySlot> getOpViewSlot(Operation *op,
+                                        const MemorySlot &rootSlot);
+
+/// Converts `slotValue` (typed at `rootSlot.elemType`) to the type at which
+/// `viewPtr` aliases `rootSlot`, by chaining
+/// `PromotableOpInterface::convertSlotValue` calls along the view chain
+/// root-to-leaf. Returns `nullptr` if any step's converter fails.
+Value convertSlotValueToViewValue(Value slotValue, Value viewPtr,
+                                  const MemorySlot &rootSlot,
+                                  OpBuilder &builder);
+
+/// Inverse of `convertSlotValueToViewValue`: converts `viewValue` back to
+/// `rootSlot.elemType` along the chain leaf-to-root.
+Value convertViewValueToSlotValue(Value viewValue, Value viewPtr,
+                                  const MemorySlot &rootSlot,
+                                  OpBuilder &builder);
+
+} // namespace mlir
+
 #endif // MLIR_INTERFACES_MEMORYSLOTINTERFACES_H
diff --git a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
index 801555fba4947..a8084ab7bf189 100644
--- a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
+++ b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
@@ -263,6 +263,44 @@ def PromotableOpInterface : OpInterface<"PromotableOpInterface"> {
       (ins "::llvm::ArrayRef<std::pair<::mlir::Operation*, ::mlir::Value>>":$mutatedDefs,
            "::mlir::OpBuilder &":$builder), [{}], [{ return; }]
     >,
+    InterfaceMethod<[{
+        Describes this operation as a transparent view of a memory slot
+        reached through one of its operands.
+
+        The returned `view.ptr` must be a result of this operation;
+        `view.elemType` is the type at which `view.ptr` aliases the slot
+        pointed to by `slotPointerOperand`, possibly different from the
+        underlying slot's element type.
+
+        Returning a view here implies `convertSlotValue` can bridge
+        between `slotPointerOperand`'s element type and `view.elemType`
+        in both directions; if no such conversion exists, return
+        `std::nullopt`.
+
+        No IR mutation is allowed in this method.
+      }],
+      "::std::optional<::mlir::PromotableSlotView>",
+      "getPromotableSlotView",
+      (ins), [{}],
+      [{ return std::nullopt; }]
+    >,
+    InterfaceMethod<[{
+        Builds a value of `targetType` from `value`, bridging the
+        underlying slot's element type and the view's element type.
+        Mem2reg calls this in both directions (load: slot → view; store:
+        view → slot).
+      }],
+      "::mlir::Value",
+      "convertSlotValue",
+      (ins "::mlir::Value":$value,
+           "::mlir::Type":$targetType,
+           "::mlir::OpBuilder &":$builder), [{}],
+      [{
+        if (value.getType() == targetType)
+          return value;
+        return ::mlir::Value{};
+      }]
+    >,
   ];
 }
 
diff --git a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
index 2c9e23250e9ee..1e1961eeca07c 100644
--- a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
+++ b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
@@ -8,5 +8,114 @@
 
 #include "mlir/Interfaces/MemorySlotInterfaces.h"
 
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/SmallVector.h"
+
 #include "mlir/Interfaces/MemorySlotOpInterfaces.cpp.inc"
 #include "mlir/Interfaces/MemorySlotTypeInterfaces.cpp.inc"
+
+using namespace mlir;
+
+namespace {
+/// One step in a view chain, leaf-first. `inputElemType` is the elemType
+/// of the slot one step closer to root; `outputElemType` is the elemType
+/// this step exposes.
+struct ViewStep {
+  PromotableOpInterface view;
+  Type inputElemType;
+  Type outputElemType;
+};
+} // namespace
+
+/// Walks back from `value` to `rootSlot.ptr` along
+/// `getPromotableSlotView` chains. On success, populates `chainOut` with
+/// the view ops leaf-to-root and writes the type at which `value` aliases
+/// the underlying slot to `*outViewElemType`.
+static bool walkPromotableSlotViewChain(Value value, const MemorySlot &rootSlot,
+                                        SmallVectorImpl<ViewStep> &chainOut,
+                                        Type *outViewElemType) {
+  if (value == rootSlot.ptr) {
+    if (outViewElemType)
+      *outViewElemType = rootSlot.elemType;
+    return true;
+  }
+
+  Value current = value;
+  Type aliasElemType{};
+  llvm::SmallPtrSet<Value, 4> seen;
+  while (current != rootSlot.ptr) {
+    if (!seen.insert(current).second)
+      return false;
+    auto promotable =
+        dyn_cast_or_null<PromotableOpInterface>(current.getDefiningOp());
+    if (!promotable)
+      return false;
+    std::optional<PromotableSlotView> info = promotable.getPromotableSlotView();
+    if (!info || info->view.ptr != current)
+      return false;
+    if (!aliasElemType)
+      aliasElemType = info->view.elemType;
+    chainOut.push_back(ViewStep{promotable, /*inputElemType=*/Type{},
+                                /*outputElemType=*/info->view.elemType});
+    current = info->slotPointerOperand;
+  }
+
+  // Fill in each step's `inputElemType` from the previous step's output
+  // (or `rootSlot.elemType` for the root-most step).
+  Type prevOutput = rootSlot.elemType;
+  for (ViewStep &step : llvm::reverse(chainOut)) {
+    step.inputElemType = prevOutput;
+    prevOutput = step.outputElemType;
+  }
+
+  if (outViewElemType)
+    *outViewElemType = aliasElemType ? aliasElemType : rootSlot.elemType;
+  return true;
+}
+
+bool mlir::isPromotableSlotView(Value value, const MemorySlot &rootSlot,
+                                Type *outViewElemType) {
+  SmallVector<ViewStep> chain;
+  return walkPromotableSlotViewChain(value, rootSlot, chain, outViewElemType);
+}
+
+std::optional<MemorySlot> mlir::getOpViewSlot(Operation *op,
+                                              const MemorySlot &rootSlot) {
+  for (Value operand : op->getOperands()) {
+    Type viewElemType;
+    if (isPromotableSlotView(operand, rootSlot, &viewElemType))
+      return MemorySlot{operand, viewElemType};
+  }
+  return std::nullopt;
+}
+
+Value mlir::convertSlotValueToViewValue(Value slotValue, Value viewPtr,
+                                        const MemorySlot &rootSlot,
+                                        OpBuilder &builder) {
+  SmallVector<ViewStep> chain;
+  if (!walkPromotableSlotViewChain(viewPtr, rootSlot, chain, /*out=*/nullptr))
+    return {};
+  Value current = slotValue;
+  // Root-to-leaf walk: reverse the leaf-first chain.
+  for (ViewStep &step : llvm::reverse(chain)) {
+    current = step.view.convertSlotValue(current, step.outputElemType, builder);
+    if (!current)
+      return {};
+  }
+  return current;
+}
+
+Value mlir::convertViewValueToSlotValue(Value viewValue, Value viewPtr,
+                                        const MemorySlot &rootSlot,
+                                        OpBuilder &builder) {
+  SmallVector<ViewStep> chain;
+  if (!walkPromotableSlotViewChain(viewPtr, rootSlot, chain, /*out=*/nullptr))
+    return {};
+  Value current = viewValue;
+  for (ViewStep &step : chain) {
+    current = step.view.convertSlotValue(current, step.inputElemType, builder);
+    if (!current)
+      return {};
+  }
+  return current;
+}
diff --git a/mlir/lib/Transforms/Mem2Reg.cpp b/mlir/lib/Transforms/Mem2Reg.cpp
index 40d08d869a9e2..4f7039bede304 100644
--- a/mlir/lib/Transforms/Mem2Reg.cpp
+++ b/mlir/lib/Transforms/Mem2Reg.cpp
@@ -400,14 +400,15 @@ LogicalResult MemorySlotPromotionAnalyzer::computeBlockingUses(
         return failure();
       regionsWithDirectUse.insert(user->getParentRegion());
     } else if (auto promotable = dyn_cast<PromotableMemOpInterface>(user)) {
-      if (!promotable.canUsesBeRemoved(slot, blockingUses, newBlockingUses,
+      MemorySlot viewSlot = getOpViewSlot(user, slot).value_or(slot);
+      if (!promotable.canUsesBeRemoved(viewSlot, blockingUses, newBlockingUses,
                                        dataLayout))
         return failure();
 
       // Operations that interact with the slot's memory will be promoted using
       // a reaching definition. Therefore, the operation must be within a region
       // where the reaching definition can be computed.
-      if (promotable.storesTo(slot))
+      if (promotable.storesTo(viewSlot))
         regionsWithDirectStore.insert(user->getParentRegion());
       else
         regionsWithDirectUse.insert(user->getParentRegion());
@@ -515,11 +516,17 @@ MemorySlotPromotionAnalyzer::computeInfo() {
   // Compute the blocks containing a store for each region, either directly or
   // inherited from a nested region. As a side effect, `definingBlocks` contains
   // all regions with at least one store.
+  //
+  // Iterating `info.userToBlockingUses` lets this also pick up stores that
+  // reach the slot through chains of views (`getPromotableSlotView`).
   DenseMap<Region *, SmallPtrSet<Block *, 16>> definingBlocks;
-  for (Operation *user : slot.ptr.getUsers())
-    if (auto storeOp = dyn_cast<PromotableMemOpInterface>(user))
-      if (storeOp.storesTo(slot))
-        definingBlocks[user->getParentRegion()].insert(user->getBlock());
+  for (auto &[region, opsMap] : info.userToBlockingUses)
+    for (auto &[user, _blockingUses] : opsMap)
+      if (auto storeOp = dyn_cast<PromotableMemOpInterface>(user)) {
+        MemorySlot viewSlot = getOpViewSlot(user, slot).value_or(slot);
+        if (storeOp.storesTo(viewSlot))
+          definingBlocks[region].insert(user->getBlock());
+      }
   for (auto &[region, regionInfo] : info.regionsToPromote)
     if (regionInfo.hasValueStores)
       definingBlocks[region->getParentRegion()].insert(
@@ -550,18 +557,37 @@ Value MemorySlotPromoter::promoteInBlock(Block *block, Value reachingDef) {
       if (info.userToBlockingUses[memOp->getParentRegion()].contains(memOp))
         reachingDefs.insert({memOp, reachingDef});
 
-      if (memOp.storesTo(slot)) {
+      MemorySlot viewSlot = getOpViewSlot(memOp, slot).value_or(slot);
+      if (memOp.storesTo(viewSlot)) {
         builder.setInsertionPointAfter(memOp);
         // To not expose default value creation to the interfaces, if we have
         // no reaching definition by now, we set it to the default value.
         // This is slightly too eager as `getStored` may not need it.
         if (!reachingDef)
           reachingDef = getOrCreateDefaultValue();
-        Value stored = memOp.getStored(slot, builder, reachingDef, dataLayout);
+        Value reachingDefAtStore = reachingDef;
+        if (slot.ptr != viewSlot.ptr) {
+          // The store sees the slot at `viewSlot.elemType`; convert the
+          // reaching definition (at root elem type) before handing it to
+          // `getStored`.
+          reachingDefAtStore = convertSlotValueToViewValue(
+              reachingDef, viewSlot.ptr, slot, builder);
+          assert(reachingDefAtStore && "convertSlotValue contract violation");
+        }
+        Value stored =
+            memOp.getStored(viewSlot, builder, reachingDefAtStore, dataLayout);
         assert(stored && "a memory operation storing to a slot must provide a "
                          "new definition of the slot");
-        reachingDef = stored;
+        // `replacedValuesMap` keeps `stored` at `viewSlot.elemType` for
+        // `visitReplacedValues`; the new reaching definition is tracked at
+        // the root slot's elem type, so convert `stored` back.
         replacedValuesMap[memOp] = stored;
+        if (viewSlot.ptr != slot.ptr) {
+          stored =
+              convertViewValueToSlotValue(stored, viewSlot.ptr, slot, builder);
+          assert(stored && "convertSlotValue contract violation");
+        }
+        reachingDef = stored;
       }
     }
 
@@ -763,11 +789,22 @@ void MemorySlotPromoter::removeBlockingUses(Region *region) {
         reachingDef = getOrCreateDefaultValue();
 
       builder.setInsertionPointAfter(toPromote);
-      if (toPromoteMemOp.removeBlockingUses(slot, blockingUsesMap[toPromote],
-                                            builder, reachingDef,
-                                            dataLayout) == DeletionKind::Delete)
+      MemorySlot viewSlot = getOpViewSlot(toPromote, slot).value_or(slot);
+      Value reachingDefAtBlockingUse = reachingDef;
+      if (viewSlot.ptr != slot.ptr) {
+        // Convert the reaching definition to `viewSlot.elemType` to match
+        // what the impl sees. Skipped when the chain is empty; any cast
+        // unused by the impl will be cleaned up by DCE.
+        reachingDefAtBlockingUse = convertSlotValueToViewValue(
+            reachingDef, viewSlot.ptr, slot, builder);
+        assert(reachingDefAtBlockingUse &&
+               "convertSlotValue contract violation");
+      }
+      if (toPromoteMemOp.removeBlockingUses(
+              viewSlot, blockingUsesMap[toPromote], builder,
+              reachingDefAtBlockingUse, dataLayout) == DeletionKind::Delete)
         toErase.insert(toPromote);
-      if (toPromoteMemOp.storesTo(slot))
+      if (toPromoteMemOp.storesTo(viewSlot))
         if (Value replacedValue = replacedValuesMap[toPromoteMemOp])
           replacedValues.push_back({toPromoteMemOp, replacedValue});
       continue;
diff --git a/mlir/test/Transforms/mem2reg.mlir b/mlir/test/Transforms/mem2reg.mlir
index 94b721cf28dcf..edc0a37807b7b 100644
--- a/mlir/test/Transforms/mem2reg.mlir
+++ b/mlir/test/Transforms/mem2reg.mlir
@@ -181,3 +181,94 @@ func.func @poison_insertion_point(%val: f64) {
 ^bb3:
   return
 }
+
+// -----
+
+// Verifies that mem2reg promotes a memory slot whose stores and loads are
+// reached through a transparent view operation that exposes itself via
+// PromotableOpInterface::getPromotableSlotView. The conditional store on
+// the view in ^bb1 must be discovered as a defining block, otherwise the
+// merge point at ^bb2 would not get a block argument and the promotion
+// would silently drop the conditional update.
+
+// CHECK-LABEL: func.func @promotable_through_view
+// CHECK-SAME: (%[[A:.*]]: i32, %[[COND:.*]]: i1) -> i32
+// CHECK-NOT: test.multi_slot_alloca
+// CHECK-NOT: test.transparent_view
+// CHECK: %[[C42:.*]] = arith.constant 42 : i32
+// CHECK: cf.cond_br %[[COND]], ^[[BB1:.*]], ^[[BB2:.*]](%[[C42]] : i32)
+// CHECK: ^[[BB1]]:
+// CHECK:   cf.br ^[[BB2]](%[[A]] : i32)
+// CHECK: ^[[BB2]](%[[MERGE:.*]]: i32):
+// CHECK:   return %[[MERGE]] : i32
+func.func @promotable_through_view(%a: i32, %cond: i1) -> i32 {
+  %c42 = arith.constant 42 : i32
+  %slot = test.multi_slot_alloca : () -> memref<i32>
+  %view = test.transparent_view %slot : (memref<i32>) -> memref<i32>
+  memref.store %c42, %view[] : memref<i32>
+  cf.cond_br %cond, ^bb1, ^bb2
+^bb1:
+  memref.store %a, %view[] : memref<i32>
+  cf.br ^bb2
+^bb2:
+  %v = memref.load %view[] : memref<i32>
+  return %v : i32
+}
+
+// -----
+
+// Type-changing transparent view: the store and load see the slot at f32
+// while the underlying allocation is at i32. mem2reg materialises an
+// `unrealized_conversion_cast` (the view op's `convertSlotValue`) at the
+// store (f32 → i32 to update the reaching def at the slot's elem type) and
+// at the load (i32 → f32 to feed the load's f32 result type).
+
+// CHECK-LABEL: func.func @promotable_through_cast_view
+// CHECK-SAME: (%[[A:.*]]: f32) -> f32
+// CHECK-NOT: test.multi_slot_alloca
+// CHECK-NOT: test.transparent_cast_view
+// CHECK: %[[I32:.*]] = builtin.unrealized_conversion_cast %[[A]] : f32 to i32
+// CHECK: %{{.*}} = builtin.unrealized_conversion_cast %[[I32]] : i32 to f32
+// CHECK: return %{{.*}} : f32
+func.func @promotable_through_cast_view(%a: f32) -> f32 {
+  %slot = test.multi_slot_alloca : () -> memref<i32>
+  %view = test.transparent_cast_view %slot : (memref<i32>) -> memref<f32>
+  memref.store %a, %view[] : memref<f32>
+  %v = memref.load %view[] : memref<f32>
+  return %v : f32
+}
+
+// -----
+
+// Same as above with a conditional store across blocks. The merge-point
+// block argument is at the root slot's element type (i32), and the
+// `convertSlotValue` casts are inserted at the store sites (f32 → i32) so
+// the merge argument can carry the conditional update; the load site
+// inserts the inverse cast (i32 → f32) for its result.
+
+// CHECK-LABEL: func.func @promotable_through_cast_view_blocks
+// CHECK-SAME: (%[[A:.*]]: f32, %[[COND:.*]]: i1) -> f32
+// CHECK-NOT: test.multi_slot_alloca
+// CHECK-NOT: test.transparent_cast_view
+// CHECK: %[[CST:.*]] = arith.constant 1.000000e+00 : f32
+// CHECK: %[[CST_I32:.*]] = builtin.unrealized_conversion_cast %[[CST]] : f32 to i32
+// CHECK: cf.cond_br %[[COND]], ^[[BB1:.*]], ^[[BB2:.*]](%[[CST_I32]] : i32)
+// CHECK: ^[[BB1]]:
+// CHECK:   %[[A_I32:.*]] = builtin.unrealized_conversion_cast %[[A]] : f32 to i32
+// CHECK:   cf.br ^[[BB2]](%[[A_I32]] : i32)
+// CHECK: ^[[BB2]](%[[MERGE:.*]]: i32):
+// CHECK:   %[[MERGE_F32:.*]] = builtin.unrealized_conversion_cast %[[MERGE]] : i32 to f32
+// CHECK:   return %[[MERGE_F32]] : f32
+func.func @promotable_through_cast_view_blocks(%a: f32, %cond: i1) -> f32 {
+  %cst = arith.constant 1.0 : f32
+  %slot = test.multi_slot_alloca : () -> memref<i32>
+  %view = test.transparent_cast_view %slot : (memref<i32>) -> memref<f32>
+  memref.store %cst, %view[] : memref<f32>
+  cf.cond_br %cond, ^bb1, ^bb2
+^bb1:
+  memref.store %a, %view[] : memref<f32>
+  cf.br ^bb2
+^bb2:
+  %v = memref.load %view[] : memref<f32>
+  return %v : f32
+}
diff --git a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
index a3ff397ac26db..ca7677a9663e7 100644
--- a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
+++ b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
@@ -1769,6 +1769,62 @@ TestMultiSlotAlloca::handleDestructuringComplete(
   return createNewMultiAllocaWithoutSlot(slot, builder, *this);
 }
 
+//===----------------------------------------------------------------------===//
+// TestTransparentView
+//===----------------------------------------------------------------------===//
+
+std::optional<PromotableSlotView> TestTransparentView::getPromotableSlotView() {
+  Type elemType = cast<MemRefType>(getResult().getType()).getElementType();
+  return PromotableSlotView{getSource(), MemorySlot{getResult(), elemType}};
+}
+
+bool TestTransparentView::canUsesBeRemoved(
+    const SmallPtrSetImpl<OpOperand *> &blockingUses,
+    SmallVectorImpl<OpOperand *> &newBlockingUses,
+    const DataLayout &dataLayout) {
+  for (OpOperand &use : getResult().getUses())
+    newBlockingUses.push_back(&use);
+  return true;
+}
+
+DeletionKind TestTransparentView::removeBlockingUses(
+    const SmallPtrSetImpl<OpOperand *> &blockingUses, OpBuilder &builder) {
+  return DeletionKind::Delete;
+}
+
+//===----------------------------------------------------------------------===//
+// TestTransparentCastView
+//===----------------------------------------------------------------------===//
+
+std::optional<PromotableSlotView>
+TestTransparentCastView::getPromotableSlotView() {
+  Type elemType = cast<MemRefType>(getResult().getType()).getElementType();
+  return PromotableSlotView{getSource(), MemorySlot{getResult(), elemType}};
+}
+
+bool TestTransparentCastView::canUsesBeRemoved(
+    const SmallPtrSetImpl<OpOperand *> &blockingUses,
+    SmallVectorImpl<OpOperand *> &newBlockingUses,
+    const DataLayout &dataLayout) {
+  for (OpOperand &use : getResult().getUses())
+    newBlockingUses.push_back(&use);
+  return true;
+}
+
+DeletionKind TestTransparentCastView::removeBlockingUses(
+    const SmallPtrSetImpl<OpOperand *> &blockingUses, OpBuilder &builder) {
+  return DeletionKind::Delete;
+}
+
+Value TestTransparentCastView::convertSlotValue(Value value, Type targetType,
+                                                OpBuilder &builder) {
+  if (value.getType() == targetType)
+    return value;
+  return UnrealizedConversionCastOp::create(builder, getLoc(), targetType,
+                                            value)
+      .getResult(0);
+}
+
 namespace {
 /// Returns test dialect's memref layout for test dialect's tensor encoding when
 /// applicable.
diff --git a/mlir/test/lib/Dialect/Test/TestOps.td b/mlir/test/lib/Dialect/Test/TestOps.td
index 56db6837b870c..fad6b8ef6a60c 100644
--- a/mlir/test/lib/Dialect/Test/TestOps.td
+++ b/mlir/test/lib/Dialect/Test/TestOps.td
@@ -3941,6 +3941,32 @@ def TestMultiSlotAlloca : TEST_Op<"multi_slot_alloca",
   let assemblyFormat = "attr-dict `:` functional-type(operands, results)";
 }
 
+// Same-element-type transparent view of a memref slot. Exercises the
+// view-chain handling in mem2reg with an identity convertSlotValue.
+def TestTransparentView : TEST_Op<"transparent_view",
+    [DeclareOpInterfaceMethods<PromotableOpInterface,
+                               ["canUsesBeRemoved",
+                                "removeBlockingUses",
+                                "getPromotableSlotView"]>]> {
+  let arguments = (ins MemRefOf<[I32]>:$source);
+  let results = (outs MemRefOf<[I32]>:$result);
+  let assemblyFormat = "$source attr-dict `:` functional-type($source, $result)";
+}
+
+// Type-changing transparent view of a memref slot. The result aliases the
+// source at a different element type; convertSlotValue bridges between
+// the two element types using `builtin.unrealized_conversion_cast`.
+def TestTransparentCastView : TEST_Op<"transparent_cast_view",
+    [DeclareOpInterfaceMethods<PromotableOpInterface,
+                               ["canUsesBeRemoved",
+                                "removeBlockingUses",
+                                "getPromotableSlotView",
+                                "convertSlotValue"]>]> {
+  let arguments = (ins MemRefOf<[I32, F32]>:$source);
+  let results = (outs MemRefOf<[I32, F32]>:$result);
+  let assemblyFormat = "$source attr-dict `:` functional-type($source, $result)";
+}
+
 //===----------------------------------------------------------------------===//
 // Test allocation Ops
 //===----------------------------------------------------------------------===//

>From 8393227f28b7b4e02ea8c26762ac7affed5e9a91 Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Tue, 12 May 2026 03:23:51 -0700
Subject: [PATCH 2/5] fix handling of region

---
 mlir/lib/Transforms/Mem2Reg.cpp   |  6 +++---
 mlir/test/Transforms/mem2reg.mlir | 23 +++++++++++++++++++++++
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/mlir/lib/Transforms/Mem2Reg.cpp b/mlir/lib/Transforms/Mem2Reg.cpp
index 4f7039bede304..f8fce7fbf8431 100644
--- a/mlir/lib/Transforms/Mem2Reg.cpp
+++ b/mlir/lib/Transforms/Mem2Reg.cpp
@@ -422,8 +422,9 @@ LogicalResult MemorySlotPromotionAnalyzer::computeBlockingUses(
     for (OpOperand *blockingUse : newBlockingUses) {
       assert(llvm::is_contained(user->getResults(), blockingUse->get()));
 
+      Operation *useOwner = blockingUse->getOwner();
       SmallPtrSetImpl<OpOperand *> &newUserBlockingUseSet =
-          blockingUsesMap[blockingUse->getOwner()];
+          userToBlockingUses[useOwner->getParentRegion()][useOwner];
       newUserBlockingUseSet.insert(blockingUse);
     }
   }
@@ -793,8 +794,7 @@ void MemorySlotPromoter::removeBlockingUses(Region *region) {
       Value reachingDefAtBlockingUse = reachingDef;
       if (viewSlot.ptr != slot.ptr) {
         // Convert the reaching definition to `viewSlot.elemType` to match
-        // what the impl sees. Skipped when the chain is empty; any cast
-        // unused by the impl will be cleaned up by DCE.
+        // what `toPromoteMemOp` sees.
         reachingDefAtBlockingUse = convertSlotValueToViewValue(
             reachingDef, viewSlot.ptr, slot, builder);
         assert(reachingDefAtBlockingUse &&
diff --git a/mlir/test/Transforms/mem2reg.mlir b/mlir/test/Transforms/mem2reg.mlir
index edc0a37807b7b..aa7fe2bcb558d 100644
--- a/mlir/test/Transforms/mem2reg.mlir
+++ b/mlir/test/Transforms/mem2reg.mlir
@@ -272,3 +272,26 @@ func.func @promotable_through_cast_view_blocks(%a: f32, %cond: i1) -> f32 {
   %v = memref.load %view[] : memref<f32>
   return %v : f32
 }
+
+// -----
+
+// Regression test: the view is defined in the parent region but the store
+// owning the propagated blocking use lives in a nested region (`scf.if`).
+// The new blocking use must be registered under the owner's region, otherwise
+// `removeBlockingUses` trips the "all operations must still be in the same
+// region" invariant after `scf.if` rebuilds itself in `finalizePromotion`.
+
+// CHECK-LABEL: func.func @promotable_through_view_across_regions
+// CHECK-SAME: (%[[COND:.*]]: i1, %[[A:.*]]: i32)
+// CHECK-NOT: test.multi_slot_alloca
+// CHECK-NOT: test.transparent_view
+// CHECK-NOT: memref.store
+// CHECK: scf.if %[[COND]]
+func.func @promotable_through_view_across_regions(%cond: i1, %a: i32) {
+  %slot = test.multi_slot_alloca : () -> memref<i32>
+  %view = test.transparent_view %slot : (memref<i32>) -> memref<i32>
+  scf.if %cond {
+    memref.store %a, %view[] : memref<i32>
+  }
+  return
+}

>From 58abb9090ffcdce9e2aae10b120642b5f8f157e7 Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Wed, 13 May 2026 03:32:13 -0700
Subject: [PATCH 3/5] Move APIs in PromotableAliaserInterface  and split
 bidirectional conversion API.

Move the new APIs into a new PromotableAliaserInterface for more clarity.

Split convertSlotValue into two directional APIs:
- convertSlotValueToViewValue to be called before promoting a load on a view.
- projectViewValueToSlotValue to be called after promoting a store on a view.

projectViewValueToSlotValue also take the reaching def of the slot before the store
so that partial view can be promoted via insert/extract.
---
 .../mlir/Interfaces/MemorySlotInterfaces.h    | 29 +++++---
 .../mlir/Interfaces/MemorySlotInterfaces.td   | 70 ++++++++++++++++---
 mlir/lib/Interfaces/MemorySlotInterfaces.cpp  | 44 ++++++++----
 mlir/lib/Transforms/Mem2Reg.cpp               | 17 ++---
 mlir/test/Transforms/mem2reg.mlir             | 20 +++---
 mlir/test/lib/Dialect/Test/TestOpDefs.cpp     | 15 +++-
 mlir/test/lib/Dialect/Test/TestOps.td         | 19 ++---
 7 files changed, 154 insertions(+), 60 deletions(-)

diff --git a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
index 2163593ef823e..7f98925df6452 100644
--- a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
+++ b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
@@ -30,10 +30,11 @@ struct DestructurableMemorySlot : public MemorySlot {
   DenseMap<Attribute, Type> subelementTypes;
 };
 
-/// Description of a memory slot view produced by a `PromotableOpInterface`
-/// operation: `slotPointerOperand` is the operand viewed by the op,
-/// `view.ptr` is the result aliasing it, and `view.elemType` is the type
-/// at which `view.ptr` aliases the underlying slot.
+/// Description of a memory slot view produced by a
+/// `PromotableAliaserInterface` operation: `slotPointerOperand` is the slot
+/// pointer viewed by the op, `view.ptr` is the result aliasing it, and
+/// `view.elemType` is the type at which `view.ptr` aliases the underlying
+/// slot.
 struct PromotableSlotView {
   Value slotPointerOperand;
   MemorySlot view;
@@ -56,7 +57,7 @@ enum class DeletionKind {
 namespace mlir {
 
 /// Returns true if `value` is `rootSlot.ptr` or a transitive view of it,
-/// following `PromotableOpInterface::getPromotableSlotView` chains. The
+/// following `PromotableAliaserInterface::getPromotableSlotView` chains. The
 /// element type at which `value` aliases the slot is written to
 /// `*outViewElemType` (equal to `rootSlot.elemType` when the chain is empty).
 bool isPromotableSlotView(Value value, const MemorySlot &rootSlot,
@@ -66,22 +67,30 @@ bool isPromotableSlotView(Value value, const MemorySlot &rootSlot,
 /// (possibly transitive) view of `rootSlot.ptr`, with `elemType` equal to
 /// the type at which that operand aliases the slot. Mem2Reg uses this to
 /// hand each `PromotableMemOpInterface` op a slot description tailored to
-/// its memref operand. Returns `nullopt` if no operand is a view of
+/// its slot pointer operand. Returns `nullopt` if no operand is a view of
 /// `rootSlot`.
 std::optional<MemorySlot> getOpViewSlot(Operation *op,
                                         const MemorySlot &rootSlot);
 
 /// Converts `slotValue` (typed at `rootSlot.elemType`) to the type at which
 /// `viewPtr` aliases `rootSlot`, by chaining
-/// `PromotableOpInterface::convertSlotValue` calls along the view chain
-/// root-to-leaf. Returns `nullptr` if any step's converter fails.
+/// `PromotableAliaserInterface::projectSlotValueToViewValue` calls along
+/// the view chain root-to-leaf. Returns a null value if any step's projector
+/// fails.
 Value convertSlotValueToViewValue(Value slotValue, Value viewPtr,
                                   const MemorySlot &rootSlot,
                                   OpBuilder &builder);
 
-/// Inverse of `convertSlotValueToViewValue`: converts `viewValue` back to
-/// `rootSlot.elemType` along the chain leaf-to-root.
+/// Inverse of `convertSlotValueToViewValue`: converts `viewValue` (typed at
+/// `viewPtr`'s view element type) back to `rootSlot.elemType` along the
+/// chain leaf-to-root, by chaining
+/// `PromotableAliaserInterface::projectViewValueToSlotValue` calls.
+/// `rootReachingDef` is the current slot value at `rootSlot.elemType`. It
+/// is projected down at each intermediate level to provide the
+/// reaching definition the per-step projector needs (e.g. for a partial
+/// subview that inserts `viewValue` into `reachingDef`).
 Value convertViewValueToSlotValue(Value viewValue, Value viewPtr,
+                                  Value rootReachingDef,
                                   const MemorySlot &rootSlot,
                                   OpBuilder &builder);
 
diff --git a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
index a8084ab7bf189..33097986da4d6 100644
--- a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
+++ b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
@@ -263,6 +263,33 @@ def PromotableOpInterface : OpInterface<"PromotableOpInterface"> {
       (ins "::llvm::ArrayRef<std::pair<::mlir::Operation*, ::mlir::Value>>":$mutatedDefs,
            "::mlir::OpBuilder &":$builder), [{}], [{ return; }]
     >,
+  ];
+}
+
+def PromotableAliaserInterface : OpInterface<"PromotableAliaserInterface"> {
+  let description = [{
+    Describes an operation that produces a transparent alias of a memory slot
+    reached through one of its operands. Mem2Reg walks chains of such aliases
+    to project slot values across them, allowing the load/store operations on
+    the alias to be promoted as if they accessed the underlying slot directly.
+
+    An alias is still a blocking use of the underlying slot pointer, so the
+    operation must also implement `PromotableOpInterface` or
+    `PromotableMemOpInterface` so that mem2reg can remove the alias once the
+    slot has been promoted.
+  }];
+  let cppNamespace = "::mlir";
+
+  let verify = [{
+    if (!::mlir::isa<::mlir::PromotableOpInterface,
+                     ::mlir::PromotableMemOpInterface>($_op))
+      return $_op->emitOpError(
+          "implements `PromotableAliaserInterface` but must also implement "
+          "`PromotableOpInterface` or `PromotableMemOpInterface`.");
+    return ::mlir::success();
+  }];
+
+  let methods = [
     InterfaceMethod<[{
         Describes this operation as a transparent view of a memory slot
         reached through one of its operands.
@@ -272,26 +299,24 @@ def PromotableOpInterface : OpInterface<"PromotableOpInterface"> {
         pointed to by `slotPointerOperand`, possibly different from the
         underlying slot's element type.
 
-        Returning a view here implies `convertSlotValue` can bridge
-        between `slotPointerOperand`'s element type and `view.elemType`
-        in both directions; if no such conversion exists, return
-        `std::nullopt`.
+        Returning a view here implies the two projection methods below can
+        bridge between the slot pointer operand's element type and
+        `view.elemType`; if no such projection exists, return `std::nullopt`.
 
         No IR mutation is allowed in this method.
       }],
       "::std::optional<::mlir::PromotableSlotView>",
       "getPromotableSlotView",
-      (ins), [{}],
-      [{ return std::nullopt; }]
+      (ins)
     >,
     InterfaceMethod<[{
-        Builds a value of `targetType` from `value`, bridging the
-        underlying slot's element type and the view's element type.
-        Mem2reg calls this in both directions (load: slot → view; store:
-        view → slot).
+        Builds a value of `targetType` from `value`, projecting the slot
+        pointer operand's element type down to the view's element type.
+        Mem2Reg calls this when a load on the view needs the slot value
+        materialized at the view's element type.
       }],
       "::mlir::Value",
-      "convertSlotValue",
+      "projectSlotValueToViewValue",
       (ins "::mlir::Value":$value,
            "::mlir::Type":$targetType,
            "::mlir::OpBuilder &":$builder), [{}],
@@ -301,6 +326,29 @@ def PromotableOpInterface : OpInterface<"PromotableOpInterface"> {
         return ::mlir::Value{};
       }]
     >,
+    InterfaceMethod<[{
+        Builds a value of `targetType` from `viewValue`, projecting the
+        view's element type back to the slot pointer operand's element type.
+        Mem2Reg calls this when a store on the view needs to update the
+        slot value at the slot pointer operand's element type.
+
+        `reachingDef` is the slot value at `targetType` immediately before
+        the store. For full views it can be ignored; for partial subviews
+        (e.g. one field of an aggregate) the result is built by inserting
+        `viewValue` into `reachingDef`.
+      }],
+      "::mlir::Value",
+      "projectViewValueToSlotValue",
+      (ins "::mlir::Value":$viewValue,
+           "::mlir::Type":$targetType,
+           "::mlir::Value":$reachingDef,
+           "::mlir::OpBuilder &":$builder), [{}],
+      [{
+        if (viewValue.getType() == targetType)
+          return viewValue;
+        return ::mlir::Value{};
+      }]
+    >,
   ];
 }
 
diff --git a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
index 1e1961eeca07c..910bd2268ca78 100644
--- a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
+++ b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
@@ -17,11 +17,11 @@
 using namespace mlir;
 
 namespace {
-/// One step in a view chain, leaf-first. `inputElemType` is the elemType
-/// of the slot one step closer to root; `outputElemType` is the elemType
+/// One step in a view chain, leaf-first. `inputElemType` is the elem type
+/// of the slot one step closer to root; `outputElemType` is the elem type
 /// this step exposes.
 struct ViewStep {
-  PromotableOpInterface view;
+  PromotableAliaserInterface view;
   Type inputElemType;
   Type outputElemType;
 };
@@ -46,16 +46,16 @@ static bool walkPromotableSlotViewChain(Value value, const MemorySlot &rootSlot,
   while (current != rootSlot.ptr) {
     if (!seen.insert(current).second)
       return false;
-    auto promotable =
-        dyn_cast_or_null<PromotableOpInterface>(current.getDefiningOp());
-    if (!promotable)
+    auto aliaser =
+        dyn_cast_or_null<PromotableAliaserInterface>(current.getDefiningOp());
+    if (!aliaser)
       return false;
-    std::optional<PromotableSlotView> info = promotable.getPromotableSlotView();
+    std::optional<PromotableSlotView> info = aliaser.getPromotableSlotView();
     if (!info || info->view.ptr != current)
       return false;
     if (!aliasElemType)
       aliasElemType = info->view.elemType;
-    chainOut.push_back(ViewStep{promotable, /*inputElemType=*/Type{},
+    chainOut.push_back(ViewStep{aliaser, /*inputElemType=*/Type{},
                                 /*outputElemType=*/info->view.elemType});
     current = info->slotPointerOperand;
   }
@@ -98,7 +98,8 @@ Value mlir::convertSlotValueToViewValue(Value slotValue, Value viewPtr,
   Value current = slotValue;
   // Root-to-leaf walk: reverse the leaf-first chain.
   for (ViewStep &step : llvm::reverse(chain)) {
-    current = step.view.convertSlotValue(current, step.outputElemType, builder);
+    current = step.view.projectSlotValueToViewValue(
+        current, step.outputElemType, builder);
     if (!current)
       return {};
   }
@@ -106,14 +107,33 @@ Value mlir::convertSlotValueToViewValue(Value slotValue, Value viewPtr,
 }
 
 Value mlir::convertViewValueToSlotValue(Value viewValue, Value viewPtr,
+                                        Value rootReachingDef,
                                         const MemorySlot &rootSlot,
                                         OpBuilder &builder) {
   SmallVector<ViewStep> chain;
   if (!walkPromotableSlotViewChain(viewPtr, rootSlot, chain, /*out=*/nullptr))
     return {};
-  Value current = viewValue;
-  for (ViewStep &step : chain) {
-    current = step.view.convertSlotValue(current, step.inputElemType, builder);
+
+  // Project `rootReachingDef` down to each step's input level so the
+  // per-step projector can use it (needed for partial subviews; full views
+  // ignore it). The chain is leaf-first, so `chain.back()` is root-most
+  // (its input is `rootSlot.elemType`) and `chain.front()` is leaf-most.
+  SmallVector<Value> perStepReachingDef(chain.size());
+  Value current = rootReachingDef;
+  for (int i = static_cast<int>(chain.size()) - 1; i >= 0; --i) {
+    perStepReachingDef[i] = current;
+    current = chain[i].view.projectSlotValueToViewValue(
+        current, chain[i].outputElemType, builder);
+    if (!current)
+      return {};
+  }
+
+  // Walk leaf-to-root, combining `viewValue` with the projected reaching
+  // definition at each step.
+  current = viewValue;
+  for (size_t i = 0; i < chain.size(); ++i) {
+    current = chain[i].view.projectViewValueToSlotValue(
+        current, chain[i].inputElemType, perStepReachingDef[i], builder);
     if (!current)
       return {};
   }
diff --git a/mlir/lib/Transforms/Mem2Reg.cpp b/mlir/lib/Transforms/Mem2Reg.cpp
index f8fce7fbf8431..5cd2c890b5aaf 100644
--- a/mlir/lib/Transforms/Mem2Reg.cpp
+++ b/mlir/lib/Transforms/Mem2Reg.cpp
@@ -568,12 +568,13 @@ Value MemorySlotPromoter::promoteInBlock(Block *block, Value reachingDef) {
           reachingDef = getOrCreateDefaultValue();
         Value reachingDefAtStore = reachingDef;
         if (slot.ptr != viewSlot.ptr) {
-          // The store sees the slot at `viewSlot.elemType`; convert the
+          // The store sees the slot at `viewSlot.elemType`; project the
           // reaching definition (at root elem type) before handing it to
           // `getStored`.
           reachingDefAtStore = convertSlotValueToViewValue(
               reachingDef, viewSlot.ptr, slot, builder);
-          assert(reachingDefAtStore && "convertSlotValue contract violation");
+          assert(reachingDefAtStore &&
+                 "projectSlotValueToViewValue contract violation");
         }
         Value stored =
             memOp.getStored(viewSlot, builder, reachingDefAtStore, dataLayout);
@@ -581,12 +582,12 @@ Value MemorySlotPromoter::promoteInBlock(Block *block, Value reachingDef) {
                          "new definition of the slot");
         // `replacedValuesMap` keeps `stored` at `viewSlot.elemType` for
         // `visitReplacedValues`; the new reaching definition is tracked at
-        // the root slot's elem type, so convert `stored` back.
+        // the root slot's elem type, so project `stored` back.
         replacedValuesMap[memOp] = stored;
         if (viewSlot.ptr != slot.ptr) {
-          stored =
-              convertViewValueToSlotValue(stored, viewSlot.ptr, slot, builder);
-          assert(stored && "convertSlotValue contract violation");
+          stored = convertViewValueToSlotValue(stored, viewSlot.ptr,
+                                               reachingDef, slot, builder);
+          assert(stored && "projectViewValueToSlotValue contract violation");
         }
         reachingDef = stored;
       }
@@ -793,12 +794,12 @@ void MemorySlotPromoter::removeBlockingUses(Region *region) {
       MemorySlot viewSlot = getOpViewSlot(toPromote, slot).value_or(slot);
       Value reachingDefAtBlockingUse = reachingDef;
       if (viewSlot.ptr != slot.ptr) {
-        // Convert the reaching definition to `viewSlot.elemType` to match
+        // Project the reaching definition to `viewSlot.elemType` to match
         // what `toPromoteMemOp` sees.
         reachingDefAtBlockingUse = convertSlotValueToViewValue(
             reachingDef, viewSlot.ptr, slot, builder);
         assert(reachingDefAtBlockingUse &&
-               "convertSlotValue contract violation");
+               "projectSlotValueToViewValue contract violation");
       }
       if (toPromoteMemOp.removeBlockingUses(
               viewSlot, blockingUsesMap[toPromote], builder,
diff --git a/mlir/test/Transforms/mem2reg.mlir b/mlir/test/Transforms/mem2reg.mlir
index aa7fe2bcb558d..064669534daa5 100644
--- a/mlir/test/Transforms/mem2reg.mlir
+++ b/mlir/test/Transforms/mem2reg.mlir
@@ -186,9 +186,9 @@ func.func @poison_insertion_point(%val: f64) {
 
 // Verifies that mem2reg promotes a memory slot whose stores and loads are
 // reached through a transparent view operation that exposes itself via
-// PromotableOpInterface::getPromotableSlotView. The conditional store on
-// the view in ^bb1 must be discovered as a defining block, otherwise the
-// merge point at ^bb2 would not get a block argument and the promotion
+// PromotableAliaserInterface::getPromotableSlotView. The conditional store
+// on the view in ^bb1 must be discovered as a defining block, otherwise
+// the merge point at ^bb2 would not get a block argument and the promotion
 // would silently drop the conditional update.
 
 // CHECK-LABEL: func.func @promotable_through_view
@@ -219,9 +219,10 @@ func.func @promotable_through_view(%a: i32, %cond: i1) -> i32 {
 
 // Type-changing transparent view: the store and load see the slot at f32
 // while the underlying allocation is at i32. mem2reg materialises an
-// `unrealized_conversion_cast` (the view op's `convertSlotValue`) at the
-// store (f32 → i32 to update the reaching def at the slot's elem type) and
-// at the load (i32 → f32 to feed the load's f32 result type).
+// `unrealized_conversion_cast` (the view op's `projectViewValueToSlotValue`)
+// at the store (f32 → i32 to update the reaching def at the slot's elem
+// type) and at the load (i32 → f32 via `projectSlotValueToViewValue` to feed
+// the load's f32 result type).
 
 // CHECK-LABEL: func.func @promotable_through_cast_view
 // CHECK-SAME: (%[[A:.*]]: f32) -> f32
@@ -242,9 +243,10 @@ func.func @promotable_through_cast_view(%a: f32) -> f32 {
 
 // Same as above with a conditional store across blocks. The merge-point
 // block argument is at the root slot's element type (i32), and the
-// `convertSlotValue` casts are inserted at the store sites (f32 → i32) so
-// the merge argument can carry the conditional update; the load site
-// inserts the inverse cast (i32 → f32) for its result.
+// `projectViewValueToSlotValue` casts are inserted at the store sites
+// (f32 → i32) so the merge argument can carry the conditional update; the
+// load site inserts the inverse cast (i32 → f32) for its result via
+// `projectSlotValueToViewValue`.
 
 // CHECK-LABEL: func.func @promotable_through_cast_view_blocks
 // CHECK-SAME: (%[[A:.*]]: f32, %[[COND:.*]]: i1) -> f32
diff --git a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
index ca7677a9663e7..7605172b6c3bb 100644
--- a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
+++ b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
@@ -1816,8 +1816,9 @@ DeletionKind TestTransparentCastView::removeBlockingUses(
   return DeletionKind::Delete;
 }
 
-Value TestTransparentCastView::convertSlotValue(Value value, Type targetType,
-                                                OpBuilder &builder) {
+Value TestTransparentCastView::projectSlotValueToViewValue(Value value,
+                                                           Type targetType,
+                                                           OpBuilder &builder) {
   if (value.getType() == targetType)
     return value;
   return UnrealizedConversionCastOp::create(builder, getLoc(), targetType,
@@ -1825,6 +1826,16 @@ Value TestTransparentCastView::convertSlotValue(Value value, Type targetType,
       .getResult(0);
 }
 
+Value TestTransparentCastView::projectViewValueToSlotValue(
+    Value viewValue, Type targetType, Value /*reachingDef*/,
+    OpBuilder &builder) {
+  if (viewValue.getType() == targetType)
+    return viewValue;
+  return UnrealizedConversionCastOp::create(builder, getLoc(), targetType,
+                                            viewValue)
+      .getResult(0);
+}
+
 namespace {
 /// Returns test dialect's memref layout for test dialect's tensor encoding when
 /// applicable.
diff --git a/mlir/test/lib/Dialect/Test/TestOps.td b/mlir/test/lib/Dialect/Test/TestOps.td
index fad6b8ef6a60c..029d65f071e04 100644
--- a/mlir/test/lib/Dialect/Test/TestOps.td
+++ b/mlir/test/lib/Dialect/Test/TestOps.td
@@ -3942,26 +3942,29 @@ def TestMultiSlotAlloca : TEST_Op<"multi_slot_alloca",
 }
 
 // Same-element-type transparent view of a memref slot. Exercises the
-// view-chain handling in mem2reg with an identity convertSlotValue.
+// view-chain handling in mem2reg with identity projections.
 def TestTransparentView : TEST_Op<"transparent_view",
     [DeclareOpInterfaceMethods<PromotableOpInterface,
                                ["canUsesBeRemoved",
-                                "removeBlockingUses",
-                                "getPromotableSlotView"]>]> {
+                                "removeBlockingUses"]>,
+     DeclareOpInterfaceMethods<PromotableAliaserInterface,
+                               ["getPromotableSlotView"]>]> {
   let arguments = (ins MemRefOf<[I32]>:$source);
   let results = (outs MemRefOf<[I32]>:$result);
   let assemblyFormat = "$source attr-dict `:` functional-type($source, $result)";
 }
 
 // Type-changing transparent view of a memref slot. The result aliases the
-// source at a different element type; convertSlotValue bridges between
-// the two element types using `builtin.unrealized_conversion_cast`.
+// source at a different element type; the projection methods bridge
+// between the two element types using `builtin.unrealized_conversion_cast`.
 def TestTransparentCastView : TEST_Op<"transparent_cast_view",
     [DeclareOpInterfaceMethods<PromotableOpInterface,
                                ["canUsesBeRemoved",
-                                "removeBlockingUses",
-                                "getPromotableSlotView",
-                                "convertSlotValue"]>]> {
+                                "removeBlockingUses"]>,
+     DeclareOpInterfaceMethods<PromotableAliaserInterface,
+                               ["getPromotableSlotView",
+                                "projectSlotValueToViewValue",
+                                "projectViewValueToSlotValue"]>]> {
   let arguments = (ins MemRefOf<[I32, F32]>:$source);
   let results = (outs MemRefOf<[I32, F32]>:$result);
   let assemblyFormat = "$source attr-dict `:` functional-type($source, $result)";

>From 573ccb55a61f8cc89b2072a95d9d6b9044e68178 Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Wed, 13 May 2026 04:03:40 -0700
Subject: [PATCH 4/5] rephrase descriptions

---
 .../mlir/Interfaces/MemorySlotInterfaces.h    | 50 ++++++--------
 .../mlir/Interfaces/MemorySlotInterfaces.td   | 69 ++++++++++---------
 mlir/lib/Interfaces/MemorySlotInterfaces.cpp  |  6 +-
 3 files changed, 60 insertions(+), 65 deletions(-)

diff --git a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
index 7f98925df6452..f549193a75a05 100644
--- a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
+++ b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.h
@@ -30,13 +30,13 @@ struct DestructurableMemorySlot : public MemorySlot {
   DenseMap<Attribute, Type> subelementTypes;
 };
 
-/// Description of a memory slot view produced by a
-/// `PromotableAliaserInterface` operation: `slotPointerOperand` is the slot
-/// pointer viewed by the op, `view.ptr` is the result aliasing it, and
-/// `view.elemType` is the type at which `view.ptr` aliases the underlying
-/// slot.
+/// Represent a memory slot view produced by a `PromotableAliaserInterface`
+/// operation.
 struct PromotableSlotView {
-  Value slotPointerOperand;
+  /// The slot pointer operand that is aliased by the view.
+  Value aliasedSlotPointerOperand;
+  /// The MemorySlot created by the operation that aliases the operand
+  /// MemorySlot.
   MemorySlot view;
 };
 
@@ -56,39 +56,31 @@ enum class DeletionKind {
 
 namespace mlir {
 
-/// Returns true if `value` is `rootSlot.ptr` or a transitive view of it,
-/// following `PromotableAliaserInterface::getPromotableSlotView` chains. The
-/// element type at which `value` aliases the slot is written to
-/// `*outViewElemType` (equal to `rootSlot.elemType` when the chain is empty).
+/// Returns true if `value` is `rootSlot.ptr` or a transitive view of it.
+/// If so, writes the element type of the alias to `*outViewElemType` (which
+/// defaults to `rootSlot.elemType` for the root pointer).
 bool isPromotableSlotView(Value value, const MemorySlot &rootSlot,
                           Type *outViewElemType = nullptr);
 
-/// Returns a MemorySlot whose `ptr` is the operand of `op` that is a
-/// (possibly transitive) view of `rootSlot.ptr`, with `elemType` equal to
-/// the type at which that operand aliases the slot. Mem2Reg uses this to
-/// hand each `PromotableMemOpInterface` op a slot description tailored to
-/// its slot pointer operand. Returns `nullopt` if no operand is a view of
-/// `rootSlot`.
+/// Returns a `MemorySlot` representing the operand of `op` that is a view of
+/// `rootSlot.ptr`, tailored with the view's element type. Returns `nullopt`
+/// if no operand is a view of `rootSlot`.
 std::optional<MemorySlot> getOpViewSlot(Operation *op,
                                         const MemorySlot &rootSlot);
 
-/// Converts `slotValue` (typed at `rootSlot.elemType`) to the type at which
-/// `viewPtr` aliases `rootSlot`, by chaining
-/// `PromotableAliaserInterface::projectSlotValueToViewValue` calls along
-/// the view chain root-to-leaf. Returns a null value if any step's projector
-/// fails.
+/// Projects `slotValue` (of `rootSlot.elemType`) down to the element type of
+/// `viewPtr` by chaining `projectSlotValueToViewValue` calls along the alias
+/// chain, from the original slot down to the view pointer. Returns a null
+/// value if any projection step fails.
 Value convertSlotValueToViewValue(Value slotValue, Value viewPtr,
                                   const MemorySlot &rootSlot,
                                   OpBuilder &builder);
 
-/// Inverse of `convertSlotValueToViewValue`: converts `viewValue` (typed at
-/// `viewPtr`'s view element type) back to `rootSlot.elemType` along the
-/// chain leaf-to-root, by chaining
-/// `PromotableAliaserInterface::projectViewValueToSlotValue` calls.
-/// `rootReachingDef` is the current slot value at `rootSlot.elemType`. It
-/// is projected down at each intermediate level to provide the
-/// reaching definition the per-step projector needs (e.g. for a partial
-/// subview that inserts `viewValue` into `reachingDef`).
+/// Projects `viewValue` back up to `rootSlot.elemType` by chaining
+/// `projectViewValueToSlotValue` calls backwards along the alias chain, from
+/// the view pointer up to the original slot. `rootReachingDef` provides the
+/// current slot value, which is projected down at each step to supply the
+/// required reaching definition (e.g., for partial subviews).
 Value convertViewValueToSlotValue(Value viewValue, Value viewPtr,
                                   Value rootReachingDef,
                                   const MemorySlot &rootSlot,
diff --git a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
index 33097986da4d6..85bf800d91e5c 100644
--- a/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
+++ b/mlir/include/mlir/Interfaces/MemorySlotInterfaces.td
@@ -268,15 +268,16 @@ def PromotableOpInterface : OpInterface<"PromotableOpInterface"> {
 
 def PromotableAliaserInterface : OpInterface<"PromotableAliaserInterface"> {
   let description = [{
-    Describes an operation that produces a transparent alias of a memory slot
-    reached through one of its operands. Mem2Reg walks chains of such aliases
-    to project slot values across them, allowing the load/store operations on
-    the alias to be promoted as if they accessed the underlying slot directly.
-
-    An alias is still a blocking use of the underlying slot pointer, so the
-    operation must also implement `PromotableOpInterface` or
-    `PromotableMemOpInterface` so that mem2reg can remove the alias once the
-    slot has been promoted.
+    Describes an operation that creates a transparent alias of a memory slot
+    accessed through one of its operands. Mem2Reg traverses chains of these
+    aliases to project slot values across them. This allows load and store
+    operations on the alias to be promoted as if they were directly accessing
+    the underlying slot.
+
+    Since an alias remains a blocking use of the underlying slot pointer, the
+    operation must also implement either `PromotableOpInterface` or
+    `PromotableMemOpInterface`. This ensures that mem2reg can remove the alias
+    after the slot has been promoted.
   }];
   let cppNamespace = "::mlir";
 
@@ -291,17 +292,17 @@ def PromotableAliaserInterface : OpInterface<"PromotableAliaserInterface"> {
 
   let methods = [
     InterfaceMethod<[{
-        Describes this operation as a transparent view of a memory slot
-        reached through one of its operands.
-
-        The returned `view.ptr` must be a result of this operation;
-        `view.elemType` is the type at which `view.ptr` aliases the slot
-        pointed to by `slotPointerOperand`, possibly different from the
-        underlying slot's element type.
-
-        Returning a view here implies the two projection methods below can
-        bridge between the slot pointer operand's element type and
-        `view.elemType`; if no such projection exists, return `std::nullopt`.
+        Returns a view of a parent memory slot (accessed via
+        `aliasedSlotPointerOperand`) as a new memory slot. The pointer to this
+        new slot must be a result of the current operation, and its element
+        type may differ from the parent's.
+
+        Providing a view requires implementing the two projection methods below
+        to bridge values between the parent's and the new slot's element types.
+        One method extracts the new slot's value from the parent's value, while
+        the other reconstructs the parent's value after a store to the new slot.
+        If these projections cannot be performed, this method should return
+        `std::nullopt`.
 
         No IR mutation is allowed in this method.
       }],
@@ -310,10 +311,11 @@ def PromotableAliaserInterface : OpInterface<"PromotableAliaserInterface"> {
       (ins)
     >,
     InterfaceMethod<[{
-        Builds a value of `targetType` from `value`, projecting the slot
-        pointer operand's element type down to the view's element type.
-        Mem2Reg calls this when a load on the view needs the slot value
-        materialized at the view's element type.
+        Constructs a value of `targetType` from the given `value`, projecting
+        the element type of the slot pointer operand down to the element type
+        of the view. Mem2Reg invokes this method when a load operation on the
+        view requires the slot value to be materialized with the view's
+        element type.
       }],
       "::mlir::Value",
       "projectSlotValueToViewValue",
@@ -327,15 +329,16 @@ def PromotableAliaserInterface : OpInterface<"PromotableAliaserInterface"> {
       }]
     >,
     InterfaceMethod<[{
-        Builds a value of `targetType` from `viewValue`, projecting the
-        view's element type back to the slot pointer operand's element type.
-        Mem2Reg calls this when a store on the view needs to update the
-        slot value at the slot pointer operand's element type.
-
-        `reachingDef` is the slot value at `targetType` immediately before
-        the store. For full views it can be ignored; for partial subviews
-        (e.g. one field of an aggregate) the result is built by inserting
-        `viewValue` into `reachingDef`.
+        Constructs a value of `targetType` from the given `viewValue`, projecting
+        the view's element type back to the element type of the slot pointer
+        operand. Mem2Reg invokes this method when a store operation on the view
+        needs to update the slot value using the element type of the slot
+        pointer operand.
+
+        The `reachingDef` parameter represents the slot value at `targetType`
+        immediately preceding the store. While it can be ignored for full views,
+        for partial subviews (such as a single field of an aggregate), the
+        result is constructed by inserting `viewValue` into `reachingDef`.
       }],
       "::mlir::Value",
       "projectViewValueToSlotValue",
diff --git a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
index 910bd2268ca78..2a322dd3f74a5 100644
--- a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
+++ b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
@@ -57,7 +57,7 @@ static bool walkPromotableSlotViewChain(Value value, const MemorySlot &rootSlot,
       aliasElemType = info->view.elemType;
     chainOut.push_back(ViewStep{aliaser, /*inputElemType=*/Type{},
                                 /*outputElemType=*/info->view.elemType});
-    current = info->slotPointerOperand;
+    current = info->aliasedSlotPointerOperand;
   }
 
   // Fill in each step's `inputElemType` from the previous step's output
@@ -116,8 +116,8 @@ Value mlir::convertViewValueToSlotValue(Value viewValue, Value viewPtr,
 
   // Project `rootReachingDef` down to each step's input level so the
   // per-step projector can use it (needed for partial subviews; full views
-  // ignore it). The chain is leaf-first, so `chain.back()` is root-most
-  // (its input is `rootSlot.elemType`) and `chain.front()` is leaf-most.
+  // ignore it). The chain is leaf-first, so `chain.back()` is the root slot
+  // and `chain.front()` is the leaf view.
   SmallVector<Value> perStepReachingDef(chain.size());
   Value current = rootReachingDef;
   for (int i = static_cast<int>(chain.size()) - 1; i >= 0; --i) {

>From 38fde8f3ee2607eea76667bb79cbd292eaa35dce Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Wed, 13 May 2026 04:24:01 -0700
Subject: [PATCH 5/5] use walkSlice

---
 mlir/lib/Interfaces/CMakeLists.txt           | 16 ++++++++++-
 mlir/lib/Interfaces/MemorySlotInterfaces.cpp | 30 +++++++++-----------
 2 files changed, 29 insertions(+), 17 deletions(-)

diff --git a/mlir/lib/Interfaces/CMakeLists.txt b/mlir/lib/Interfaces/CMakeLists.txt
index 41e890cb408ba..bb3a33117b912 100644
--- a/mlir/lib/Interfaces/CMakeLists.txt
+++ b/mlir/lib/Interfaces/CMakeLists.txt
@@ -100,7 +100,21 @@ add_mlir_library(MLIRLoopLikeInterface
 )
 
 add_mlir_interface_library(MemOpInterfaces)
-add_mlir_interface_library(MemorySlotInterfaces)
+
+add_mlir_library(MLIRMemorySlotInterfaces
+  MemorySlotInterfaces.cpp
+
+  ADDITIONAL_HEADER_DIRS
+  ${MLIR_MAIN_INCLUDE_DIR}/mlir/Interfaces
+
+  DEPENDS
+  MLIRMemorySlotInterfacesIncGen
+
+  LINK_LIBS PUBLIC
+  MLIRAnalysis
+  MLIRIR
+)
+
 add_mlir_interface_library(ParallelCombiningOpInterface)
 add_mlir_interface_library(RuntimeVerifiableOpInterface)
 add_mlir_interface_library(ShapedOpInterfaces)
diff --git a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
index 2a322dd3f74a5..e2276fe879d9c 100644
--- a/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
+++ b/mlir/lib/Interfaces/MemorySlotInterfaces.cpp
@@ -8,7 +8,7 @@
 
 #include "mlir/Interfaces/MemorySlotInterfaces.h"
 
-#include "llvm/ADT/SmallPtrSet.h"
+#include "mlir/Analysis/SliceWalk.h"
 #include "llvm/ADT/SmallVector.h"
 
 #include "mlir/Interfaces/MemorySlotOpInterfaces.cpp.inc"
@@ -34,31 +34,29 @@ struct ViewStep {
 static bool walkPromotableSlotViewChain(Value value, const MemorySlot &rootSlot,
                                         SmallVectorImpl<ViewStep> &chainOut,
                                         Type *outViewElemType) {
-  if (value == rootSlot.ptr) {
-    if (outViewElemType)
-      *outViewElemType = rootSlot.elemType;
-    return true;
-  }
-
-  Value current = value;
   Type aliasElemType{};
-  llvm::SmallPtrSet<Value, 4> seen;
-  while (current != rootSlot.ptr) {
-    if (!seen.insert(current).second)
-      return false;
+  bool reachedRoot = false;
+  WalkContinuation result = walkSlice(value, [&](Value current) {
+    if (current == rootSlot.ptr) {
+      reachedRoot = true;
+      return WalkContinuation::skip();
+    }
     auto aliaser =
         dyn_cast_or_null<PromotableAliaserInterface>(current.getDefiningOp());
     if (!aliaser)
-      return false;
+      return WalkContinuation::interrupt();
     std::optional<PromotableSlotView> info = aliaser.getPromotableSlotView();
     if (!info || info->view.ptr != current)
-      return false;
+      return WalkContinuation::interrupt();
     if (!aliasElemType)
       aliasElemType = info->view.elemType;
     chainOut.push_back(ViewStep{aliaser, /*inputElemType=*/Type{},
                                 /*outputElemType=*/info->view.elemType});
-    current = info->aliasedSlotPointerOperand;
-  }
+    return WalkContinuation::advanceTo(info->aliasedSlotPointerOperand);
+  });
+
+  if (result.wasInterrupted() || !reachedRoot)
+    return false;
 
   // Fill in each step's `inputElemType` from the previous step's output
   // (or `rootSlot.elemType` for the root-most step).



More information about the Mlir-commits mailing list