[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