[Mlir-commits] [mlir] [mlir][bufferization] Add edge and error case tests for static memory planner (PR #216610)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Sun Aug 16 15:20:38 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir-bufferization
Author: Krish Gupta (KrxGu)
<details>
<summary>Changes</summary>
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.
---
Full diff: https://github.com/llvm/llvm-project/pull/216610.diff
2 Files Affected:
- (modified) mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-analysis.mlir (+190-1)
- (modified) mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-errors.mlir (+39)
``````````diff
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
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/216610
More information about the Mlir-commits
mailing list