[Mlir-commits] [mlir] 5f0f4f5 - [mlir][bufferization] Add edge and error case tests for static memory planner (#216610)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Mon Aug 17 04:21:40 PDT 2026
Author: Krish Gupta
Date: 2026-08-17T16:51:35+05:30
New Revision: 5f0f4f551b7913fc203e888fa9687df754e14897
URL: https://github.com/llvm/llvm-project/commit/5f0f4f551b7913fc203e888fa9687df754e14897
DIFF: https://github.com/llvm/llvm-project/commit/5f0f4f551b7913fc203e888fa9687df754e14897.diff
LOG: [mlir][bufferization] Add edge and error case tests for static memory planner (#216610)
Extends test coverage for the static memory planner by adding cases that
were missing from the existing suite.
Eight new analysis tests (19-26):
- Static and dynamic shapes coexisting in one function
- Both branches of scf.if deallocating the same alloc
- Dealloc at depth 3 (triply nested scf.if)
- Multi-hop alias chain through two consecutive scf.if results
- arith.select feeding into an scf.if result (cross-interface alias)
- One alloc freed in scf.if body, another in the entry block — same
arena
- Dealloc inside an else branch's nested scf.if
- scf.for body that only reads entry-block buffers (the canonical valid
loop case)
Three new error tests (3-5):
- Dynamic alloc with no dealloc is silently skipped, not an error
- Walk stops on the first bad alloc and points the error there precisely
- cf.cond_br to a sibling block is rejected as unstructured control flow
No pass changes. Tests only.
Added:
Modified:
mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-analysis.mlir
mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir
Removed:
################################################################################
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
diff erent 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
diff erent 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..f16d2bd0f39d3 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,31 @@ func.func @error_escaping_dealloc(%cond: i1) {
memref.dealloc %alloc : memref<1024xf32>
return
}
+
+// -----
+
+// 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() {
+ // 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 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}}
+ %alloc = memref.alloc() : memref<1024xf32>
+ cf.cond_br %cond, ^bb1, ^bb2
+^bb1:
+ memref.dealloc %alloc : memref<1024xf32>
+ return
+^bb2:
+ return
+}
More information about the Mlir-commits
mailing list