[Mlir-commits] [mlir] [mlir][bufferization] Add edge and error case tests for static memory planner (PR #216610)

Krish Gupta llvmlistbot at llvm.org
Sun Aug 16 15:19:58 PDT 2026


https://github.com/KrxGu created https://github.com/llvm/llvm-project/pull/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.

>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] [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
+}



More information about the Mlir-commits mailing list