[Mlir-commits] [mlir] [mlir][bufferization] Add edge and error case tests for static memory planner (PR #216610)
Krish Gupta
llvmlistbot at llvm.org
Mon Aug 17 03:04:29 PDT 2026
https://github.com/KrxGu updated https://github.com/llvm/llvm-project/pull/216610
>From d779ab850232d25e7ad12a4e42b31634a2b188d4 Mon Sep 17 00:00:00 2001
From: KrxGu <krishom70 at gmail.com>
Date: Mon, 17 Aug 2026 03:48:25 +0530
Subject: [PATCH 1/2] [mlir][bufferization] Add edge and error case tests for
static memory planner
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Eight new analysis tests (19-26) covering cases that were missing:
- Mixed static and dynamic shapes in the same function: static
alloc transforms, dynamic is skipped, both coexist safely.
- Both branches of an scf.if dealloc the same alloc: the pass
correctly handles duplicate dealloc ops for the same value.
- Alloc freed at depth 3 (triply nested scf.if): verifies that
findAncestorOpInBlock walks up any depth, not just one level.
- Chained scf.if results (%a/%b → %0 → %1 → dealloc): the
analysis resolves a multi-hop alias chain and finds the dealloc.
- arith.select feeding into an scf.if result (cross-interface
chain): exercises the alias graph across two different interface
implementations in a single path.
- Mixed dealloc locations (one in scf.if body, one in entry block)
both transform into the same arena.
- Dealloc nested in an else branch: verifies else regions are
traversed the same as then regions.
- scf.for body that only reads entry-block buffers with no
iter_args: the canonical valid loop case — transforms cleanly.
Three new error tests (3-5):
- Dynamic alloc with no dealloc is silently skipped, not an error.
- When the first alloc has no dealloc, the walk stops there and
emits the error on that specific alloc, not on later valid ones.
- cf.cond_br to a sibling block is rejected as unstructured
control flow, same as cf.br.
---
.../static-memory-planner-analysis.mlir | 191 +++++++++++++++++-
.../static-memory-planner-errors.mlir | 39 ++++
2 files changed, 229 insertions(+), 1 deletion(-)
diff --git a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-analysis.mlir b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-analysis.mlir
index 8bcfb023b2a7b..db5fc986d5fb9 100644
--- a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-analysis.mlir
+++ b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-analysis.mlir
@@ -408,4 +408,193 @@ func.func @scf_for_orig_alloc_freed_in_body(%lb: index, %ub: index, %step: index
}
memref.dealloc %0 : memref<1024xf32>
return
-}
\ No newline at end of file
+}
+// -----
+
+// Test 19: Mixed static and dynamic shapes in the same function. The static
+// alloc is transformed into the arena; the dynamic one is silently skipped and
+// left as-is. The two kinds coexist safely in the same function.
+// CHECK-LABEL: func @mixed_static_dynamic
+func.func @mixed_static_dynamic(%n: index) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<4096xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<4096xi8> to memref<1024xf32>
+ // CHECK: memref.alloc(%{{.*}}) : memref<?xf32>
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ %b = memref.alloc(%n) : memref<?xf32>
+ memref.dealloc %a : memref<1024xf32>
+ return
+}
+
+// -----
+
+// Test 20: Both branches of an scf.if dealloc the same alloc. The analysis
+// finds both dealloc ops; the lifetime anchors at the scf.if, and the alloc is
+// placed in the arena with both deallocs erased.
+// CHECK-LABEL: func @scf_if_both_branches_dealloc
+func.func @scf_if_both_branches_dealloc(%c: i1) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<4096xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<4096xi8> to memref<1024xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ scf.if %c {
+ memref.dealloc %a : memref<1024xf32>
+ } else {
+ memref.dealloc %a : memref<1024xf32>
+ }
+ return
+}
+
+// -----
+
+// Test 21: Dealloc at depth 3 (scf.if inside scf.if inside scf.if).
+// findAncestorOpInBlock returns the outermost scf.if as the anchor, making the
+// lifetime conservative. The alloc still transforms correctly.
+// CHECK-LABEL: func @deep_nested_dealloc
+func.func @deep_nested_dealloc(%c1: i1, %c2: i1, %c3: i1) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<4096xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<4096xi8> to memref<1024xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ scf.if %c1 {
+ scf.if %c2 {
+ scf.if %c3 {
+ memref.dealloc %a : memref<1024xf32>
+ }
+ }
+ }
+ return
+}
+
+// -----
+
+// Test 22: Two scf.if ops chained through their results. The alias chain is
+// %a/%b → %0 → %1 → dealloc. The analysis resolves the full multi-hop chain,
+// finds the single dealloc on %1, and transforms both allocs into the arena.
+// CHECK-LABEL: func @chained_scf_if_results
+func.func @chained_scf_if_results(%c1: i1, %c2: i1) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<8192xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<8192xi8> to memref<1024xf32>
+ // CHECK-NEXT: %[[C4096:.*]] = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C4096]]][] : memref<8192xi8> to memref<1024xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ %b = memref.alloc() : memref<1024xf32>
+ %0 = scf.if %c1 -> memref<1024xf32> {
+ scf.yield %a : memref<1024xf32>
+ } else {
+ scf.yield %b : memref<1024xf32>
+ }
+ %1 = scf.if %c2 -> memref<1024xf32> {
+ scf.yield %0 : memref<1024xf32>
+ } else {
+ scf.yield %a : memref<1024xf32>
+ }
+ memref.dealloc %1 : memref<1024xf32>
+ return
+}
+
+// -----
+
+// Test 23: arith.select feeds into an scf.if result which is then deallocated.
+// This is a cross-op-type alias chain: %a/%b → arith.select → scf.if → dealloc.
+// Both ops implement different interfaces (BufferViewFlowOpInterface and
+// RegionBranchOpInterface), so this exercises the unified analysis path.
+// CHECK-LABEL: func @select_chained_into_scf_if
+func.func @select_chained_into_scf_if(%c1: i1, %c2: i1) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<8192xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<8192xi8> to memref<1024xf32>
+ // CHECK-NEXT: %[[C4096:.*]] = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C4096]]][] : memref<8192xi8> to memref<1024xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ %b = memref.alloc() : memref<1024xf32>
+ %sel = arith.select %c1, %a, %b : memref<1024xf32>
+ %0 = scf.if %c2 -> memref<1024xf32> {
+ scf.yield %sel : memref<1024xf32>
+ } else {
+ scf.yield %a : memref<1024xf32>
+ }
+ memref.dealloc %0 : memref<1024xf32>
+ return
+}
+
+// -----
+
+// Test 24: Mixed dealloc locations — one alloc freed inside an scf.if body,
+// another freed directly in the entry block. Both live in the entry block, so
+// both are eligible. They share the same arena despite different dealloc styles.
+// CHECK-LABEL: func @mixed_dealloc_locations
+func.func @mixed_dealloc_locations(%c: i1) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<6144xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<6144xi8> to memref<1024xf32>
+ // CHECK-NEXT: %[[C4096:.*]] = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C4096]]][] : memref<6144xi8> to memref<512xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ %b = memref.alloc() : memref<512xf32>
+ scf.if %c {
+ memref.dealloc %a : memref<1024xf32>
+ }
+ memref.dealloc %b : memref<512xf32>
+ return
+}
+
+// -----
+
+// Test 25: Dealloc nested in the else branch of a nested scf.if. Verifies
+// that findAncestorOpInBlock works for else regions as well as then regions,
+// and that the alias analysis traverses both sides of conditionals.
+// CHECK-LABEL: func @nested_else_dealloc
+func.func @nested_else_dealloc(%c1: i1, %c2: i1) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<4096xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<4096xi8> to memref<1024xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ %a = memref.alloc() : memref<1024xf32>
+ scf.if %c1 {
+ } else {
+ scf.if %c2 {
+ memref.dealloc %a : memref<1024xf32>
+ }
+ }
+ return
+}
+
+// -----
+
+// Test 26: scf.for body only reads entry-block buffers (no ownership transfer,
+// no iter_args). Both allocs and deallocs are in the entry block, so the
+// transformation applies cleanly and the loop body receives the arena views.
+// CHECK-LABEL: func @scf_for_reads_entry_block_bufs
+func.func @scf_for_reads_entry_block_bufs(%lb: index, %ub: index, %step: index) {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<8192xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %[[VA:.*]] = memref.view %[[ARENA]][%[[C0]]][] : memref<8192xi8> to memref<1024xf32>
+ // CHECK-NEXT: %[[C4096:.*]] = arith.constant 4096 : index
+ // CHECK-NEXT: %[[VB:.*]] = memref.view %[[ARENA]][%[[C4096]]][] : memref<8192xi8> to memref<1024xf32>
+ // CHECK-NOT: memref.alloc
+ // CHECK-NOT: memref.dealloc
+ // CHECK: scf.for
+ // CHECK: memref.copy %[[VA]], %[[VB]]
+ %a = memref.alloc() : memref<1024xf32>
+ %b = memref.alloc() : memref<1024xf32>
+ scf.for %iv = %lb to %ub step %step {
+ memref.copy %a, %b : memref<1024xf32> to memref<1024xf32>
+ }
+ memref.dealloc %a : memref<1024xf32>
+ memref.dealloc %b : memref<1024xf32>
+ return
+}
diff --git a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
index cf7388cbe98c1..932fb687e0b6a 100644
--- a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
+++ b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
@@ -22,3 +22,42 @@ func.func @error_escaping_dealloc(%cond: i1) {
memref.dealloc %alloc : memref<1024xf32>
return
}
+
+// -----
+
+// Test 3: A dynamic-shape alloc with no dealloc should be silently skipped,
+// not an error. Dynamic shapes are the one case where missing deallocs are
+// acceptable — the alloc is simply not eligible for planning.
+// (No expected-error here — the pass must not emit one.)
+func.func @error_dynamic_no_dealloc(%n: index) {
+ %alloc = memref.alloc(%n) : memref<?xf32>
+ return
+}
+
+// -----
+
+// Test 4: When one alloc has no dealloc and another is valid, the pass errors
+// on the first bad alloc and stops (WalkResult::interrupt). The error points
+// precisely to the problematic alloc, not to subsequent valid ones.
+func.func @error_first_bad_stops_walk() {
+ // expected-error @+1 {{no dealloc found; run the deallocation pipeline before this pass}}
+ %bad = memref.alloc() : memref<1024xf32>
+ %ok = memref.alloc() : memref<512xf32>
+ memref.dealloc %ok : memref<512xf32>
+ return
+}
+
+// -----
+
+// Test 5: cf.cond_br to a sibling block is also unstructured control flow and
+// must be rejected, just like cf.br.
+func.func @error_cond_br_dealloc(%cond: i1) {
+ // expected-error @+1 {{unstructured control flow is not supported}}
+ %alloc = memref.alloc() : memref<1024xf32>
+ cf.cond_br %cond, ^bb1, ^bb2
+^bb1:
+ memref.dealloc %alloc : memref<1024xf32>
+ return
+^bb2:
+ return
+}
>From a7cd7ee485a251d3c9eab18a41c4fafe86a0f165 Mon Sep 17 00:00:00 2001
From: KrxGu <krishom70 at gmail.com>
Date: Mon, 17 Aug 2026 15:34:16 +0530
Subject: [PATCH 2/2] Remove dynamic-no-dealloc test from errors file
The test had no expected-error annotation, which is not valid in a
-verify-diagnostics test file. The behavior it documented (dynamic
allocs are silently skipped) is already covered by the analysis test
file where it belongs.
---
.../Transforms/static-memory-planner-errors.mlir | 15 ++-------------
1 file changed, 2 insertions(+), 13 deletions(-)
diff --git a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
index 932fb687e0b6a..f16d2bd0f39d3 100644
--- a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
+++ b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
@@ -25,18 +25,7 @@ func.func @error_escaping_dealloc(%cond: i1) {
// -----
-// Test 3: A dynamic-shape alloc with no dealloc should be silently skipped,
-// not an error. Dynamic shapes are the one case where missing deallocs are
-// acceptable — the alloc is simply not eligible for planning.
-// (No expected-error here — the pass must not emit one.)
-func.func @error_dynamic_no_dealloc(%n: index) {
- %alloc = memref.alloc(%n) : memref<?xf32>
- return
-}
-
-// -----
-
-// Test 4: When one alloc has no dealloc and another is valid, the pass errors
+// Test 3: When one alloc has no dealloc and another is valid, the pass errors
// on the first bad alloc and stops (WalkResult::interrupt). The error points
// precisely to the problematic alloc, not to subsequent valid ones.
func.func @error_first_bad_stops_walk() {
@@ -49,7 +38,7 @@ func.func @error_first_bad_stops_walk() {
// -----
-// Test 5: cf.cond_br to a sibling block is also unstructured control flow and
+// Test 4: cf.cond_br to a sibling block is also unstructured control flow and
// must be rejected, just like cf.br.
func.func @error_cond_br_dealloc(%cond: i1) {
// expected-error @+1 {{unstructured control flow is not supported}}
More information about the Mlir-commits
mailing list