[llvm] [LoopFusion] Validate loop structure before creating LoopCandidates (PR #192280)

Madhur Amilkanthwar via llvm-commits llvm-commits at lists.llvm.org
Thu Apr 16 01:22:34 PDT 2026


https://github.com/madhur13490 updated https://github.com/llvm/llvm-project/pull/192280

>From 5730f94cc9ef9dc7ea67586447bba8df3d9307a6 Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Tue, 3 Mar 2026 23:19:46 -0800
Subject: [PATCH 1/2] [LoopFusion] Validate loop structure before creating
 LoopCandidates

This patch add early check in LoopFuse.cpp
to skip loops missing preheader, header, exiting,
exit block or latch. It also makes `reportInvalidCandidate`
gracefully return false instead of asserting when Preheader
is null.

In other words this patch skips loops which lack the required structure.

Fixes #156670.
---
 llvm/lib/Transforms/Scalar/LoopFuse.cpp       | 12 +++-
 .../test/Transforms/LoopFusion/indirect-br.ll | 59 +++++++++++++++++++
 2 files changed, 70 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/Transforms/LoopFusion/indirect-br.ll

diff --git a/llvm/lib/Transforms/Scalar/LoopFuse.cpp b/llvm/lib/Transforms/Scalar/LoopFuse.cpp
index b577bf6584615..9c7500b0a290e 100644
--- a/llvm/lib/Transforms/Scalar/LoopFuse.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopFuse.cpp
@@ -363,7 +363,9 @@ struct FusionCandidate {
 
   bool reportInvalidCandidate(Statistic &Stat) const {
     using namespace ore;
-    assert(L && Preheader && "Fusion candidate not initialized properly!");
+    assert(L && "Fusion candidate has null loop!");
+    if (!Preheader)
+      return false;
 #if LLVM_ENABLE_STATS
     ++Stat;
     ORE.emit(OptimizationRemarkAnalysis(DEBUG_TYPE, Stat.getName(),
@@ -573,6 +575,14 @@ struct LoopFuser {
   /// Flow Equivalent sets, sorted by dominance.
   void collectFusionCandidates(const LoopVector &LV) {
     for (Loop *L : LV) {
+      // Skip loops that do not have the structure required for fusion (e.g.
+      // loops with indirectbr may lack a preheader). Creating a FusionCandidate
+      // for such loops can lead to reportInvalidCandidate being called with
+      // a null Preheader.
+      if (!L->getLoopPreheader() || !L->getHeader() || !L->getExitingBlock() ||
+          !L->getExitBlock() || !L->getLoopLatch())
+        continue;
+
       TTI::PeelingPreferences PP =
           gatherPeelingPreferences(L, SE, TTI, std::nullopt, std::nullopt);
       FusionCandidate CurrCand(L, DT, &PDT, ORE, PP);
diff --git a/llvm/test/Transforms/LoopFusion/indirect-br.ll b/llvm/test/Transforms/LoopFusion/indirect-br.ll
new file mode 100644
index 0000000000000..2f95a3a6c4b6f
--- /dev/null
+++ b/llvm/test/Transforms/LoopFusion/indirect-br.ll
@@ -0,0 +1,59 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -S -passes=loop-fusion < %s | FileCheck %s
+; Test that loop-fusion does not crash on functions with indirectbr and
+; loops that lack a preheader (or other required loop structure).
+
+define i32 @foo(ptr nocapture readonly %p) {
+; CHECK-LABEL: define i32 @foo(
+; CHECK-SAME: ptr readonly captures(none) [[P:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[TBL:%.*]] = alloca <2 x ptr>, align 16
+; CHECK-NEXT:    store <2 x ptr> <ptr blockaddress(@foo, %[[INDIRECT:.*]]), ptr blockaddress(@foo, %[[END:.*]])>, ptr [[TBL]], align 16
+; CHECK-NEXT:    br label %[[FOR_COND:.*]]
+; CHECK:       [[FOR_COND]]:
+; CHECK-NEXT:    [[CUR:%.*]] = phi ptr [ [[P]], %[[ENTRY]] ], [ [[NEXT:%.*]], %[[FOR_COND]] ]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i8, ptr [[CUR]], align 1
+; CHECK-NEXT:    [[CMP:%.*]] = icmp eq i8 [[TMP0]], 7
+; CHECK-NEXT:    [[NEXT]] = getelementptr inbounds i8, ptr [[CUR]], i64 1
+; CHECK-NEXT:    br i1 [[CMP]], label %[[INDIRECT_PREHEADER:.*]], label %[[FOR_COND]]
+; CHECK:       [[INDIRECT_PREHEADER]]:
+; CHECK-NEXT:    [[TMP1:%.*]] = load i8, ptr [[NEXT]], align 1
+; CHECK-NEXT:    [[IDX:%.*]] = sext i8 [[TMP1]] to i64
+; CHECK-NEXT:    [[SLOT:%.*]] = getelementptr inbounds <2 x ptr>, ptr [[TBL]], i64 0, i64 [[IDX]]
+; CHECK-NEXT:    [[TMP2:%.*]] = load ptr, ptr [[SLOT]], align 8
+; CHECK-NEXT:    br label %[[INDIRECT]]
+; CHECK:       [[INDIRECT]]:
+; CHECK-NEXT:    indirectbr ptr [[TMP2]], [label %[[INDIRECT]], label %[[END]]]
+; CHECK:       [[INDIRECT2:.*:]]
+; CHECK-NEXT:    indirectbr ptr [[TMP2]], [label %[[INDIRECT]], label %[[END]]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i32 0
+;
+entry:
+  %tbl = alloca <2 x ptr>, align 16
+  store <2 x ptr> <ptr blockaddress(@foo, %indirect), ptr blockaddress(@foo, %exit)>, ptr %tbl, align 16
+  br label %for.cond
+
+for.cond:                                         ; preds = %for.cond, %entry
+  %cur = phi ptr [ %p, %entry ], [ %next, %for.cond ]
+  %val = load i8, ptr %cur, align 1
+  %cmp = icmp eq i8 %val, 7
+  %next = getelementptr inbounds i8, ptr %cur, i64 1
+  br i1 %cmp, label %indirect.preheader, label %for.cond
+
+indirect.preheader:                               ; preds = %for.cond
+  %raw = load i8, ptr %next, align 1
+  %idx = sext i8 %raw to i64
+  %slot = getelementptr inbounds <2 x ptr>, ptr %tbl, i64 0, i64 %idx
+  %dest = load ptr, ptr %slot, align 8
+  br label %indirect
+
+indirect:
+  indirectbr ptr %dest, [label %indirect, label %exit]
+
+indirect2:
+  indirectbr ptr %dest, [label %indirect, label %exit]
+
+exit:
+  ret i32 0
+}

>From b732bc5b0405bd7649b5ef3a2516da97d9b8c955 Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Thu, 16 Apr 2026 01:21:49 -0700
Subject: [PATCH 2/2] fixup! address review comments

---
 llvm/lib/Transforms/Scalar/LoopFuse.cpp       | 19 ++++++-------------
 .../test/Transforms/LoopFusion/indirect-br.ll | 18 +++++++++---------
 2 files changed, 15 insertions(+), 22 deletions(-)

diff --git a/llvm/lib/Transforms/Scalar/LoopFuse.cpp b/llvm/lib/Transforms/Scalar/LoopFuse.cpp
index 9c7500b0a290e..be543062670c6 100644
--- a/llvm/lib/Transforms/Scalar/LoopFuse.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopFuse.cpp
@@ -363,14 +363,15 @@ struct FusionCandidate {
 
   bool reportInvalidCandidate(Statistic &Stat) const {
     using namespace ore;
-    assert(L && "Fusion candidate has null loop!");
-    if (!Preheader)
-      return false;
+    ORE.emit(OptimizationRemarkAnalysis(DEBUG_TYPE, "InvalidCandidate",
+                                        L->getStartLoc(), L->getHeader())
+             << "Loop is not a candidate for fusion");
+
 #if LLVM_ENABLE_STATS
     ++Stat;
     ORE.emit(OptimizationRemarkAnalysis(DEBUG_TYPE, Stat.getName(),
-                                        L->getStartLoc(), Preheader)
-             << "[" << Preheader->getParent()->getName() << "]: "
+                                        L->getStartLoc(), L->getHeader())
+             << "[" << L->getHeader()->getParent()->getName() << "]: "
              << "Loop is not a candidate for fusion: " << Stat.getDesc());
 #endif
     return false;
@@ -575,14 +576,6 @@ struct LoopFuser {
   /// Flow Equivalent sets, sorted by dominance.
   void collectFusionCandidates(const LoopVector &LV) {
     for (Loop *L : LV) {
-      // Skip loops that do not have the structure required for fusion (e.g.
-      // loops with indirectbr may lack a preheader). Creating a FusionCandidate
-      // for such loops can lead to reportInvalidCandidate being called with
-      // a null Preheader.
-      if (!L->getLoopPreheader() || !L->getHeader() || !L->getExitingBlock() ||
-          !L->getExitBlock() || !L->getLoopLatch())
-        continue;
-
       TTI::PeelingPreferences PP =
           gatherPeelingPreferences(L, SE, TTI, std::nullopt, std::nullopt);
       FusionCandidate CurrCand(L, DT, &PDT, ORE, PP);
diff --git a/llvm/test/Transforms/LoopFusion/indirect-br.ll b/llvm/test/Transforms/LoopFusion/indirect-br.ll
index 2f95a3a6c4b6f..688338bfb77fc 100644
--- a/llvm/test/Transforms/LoopFusion/indirect-br.ll
+++ b/llvm/test/Transforms/LoopFusion/indirect-br.ll
@@ -8,25 +8,25 @@ define i32 @foo(ptr nocapture readonly %p) {
 ; CHECK-SAME: ptr readonly captures(none) [[P:%.*]]) {
 ; CHECK-NEXT:  [[ENTRY:.*]]:
 ; CHECK-NEXT:    [[TBL:%.*]] = alloca <2 x ptr>, align 16
-; CHECK-NEXT:    store <2 x ptr> <ptr blockaddress(@foo, %[[INDIRECT:.*]]), ptr blockaddress(@foo, %[[END:.*]])>, ptr [[TBL]], align 16
+; CHECK-NEXT:    store <2 x ptr> <ptr blockaddress(@foo, %[[INDIRECT:.*]]), ptr blockaddress(@foo, %[[EXIT:.*]])>, ptr [[TBL]], align 16
 ; CHECK-NEXT:    br label %[[FOR_COND:.*]]
 ; CHECK:       [[FOR_COND]]:
 ; CHECK-NEXT:    [[CUR:%.*]] = phi ptr [ [[P]], %[[ENTRY]] ], [ [[NEXT:%.*]], %[[FOR_COND]] ]
-; CHECK-NEXT:    [[TMP0:%.*]] = load i8, ptr [[CUR]], align 1
-; CHECK-NEXT:    [[CMP:%.*]] = icmp eq i8 [[TMP0]], 7
+; CHECK-NEXT:    [[VAL:%.*]] = load i8, ptr [[CUR]], align 1
+; CHECK-NEXT:    [[CMP:%.*]] = icmp eq i8 [[VAL]], 7
 ; CHECK-NEXT:    [[NEXT]] = getelementptr inbounds i8, ptr [[CUR]], i64 1
 ; CHECK-NEXT:    br i1 [[CMP]], label %[[INDIRECT_PREHEADER:.*]], label %[[FOR_COND]]
 ; CHECK:       [[INDIRECT_PREHEADER]]:
-; CHECK-NEXT:    [[TMP1:%.*]] = load i8, ptr [[NEXT]], align 1
-; CHECK-NEXT:    [[IDX:%.*]] = sext i8 [[TMP1]] to i64
+; CHECK-NEXT:    [[RAW:%.*]] = load i8, ptr [[NEXT]], align 1
+; CHECK-NEXT:    [[IDX:%.*]] = sext i8 [[RAW]] to i64
 ; CHECK-NEXT:    [[SLOT:%.*]] = getelementptr inbounds <2 x ptr>, ptr [[TBL]], i64 0, i64 [[IDX]]
-; CHECK-NEXT:    [[TMP2:%.*]] = load ptr, ptr [[SLOT]], align 8
+; CHECK-NEXT:    [[DEST:%.*]] = load ptr, ptr [[SLOT]], align 8
 ; CHECK-NEXT:    br label %[[INDIRECT]]
 ; CHECK:       [[INDIRECT]]:
-; CHECK-NEXT:    indirectbr ptr [[TMP2]], [label %[[INDIRECT]], label %[[END]]]
+; CHECK-NEXT:    indirectbr ptr [[DEST]], [label %[[INDIRECT]], label %[[EXIT]]]
 ; CHECK:       [[INDIRECT2:.*:]]
-; CHECK-NEXT:    indirectbr ptr [[TMP2]], [label %[[INDIRECT]], label %[[END]]]
-; CHECK:       [[END]]:
+; CHECK-NEXT:    indirectbr ptr [[DEST]], [label %[[INDIRECT]], label %[[EXIT]]]
+; CHECK:       [[EXIT]]:
 ; CHECK-NEXT:    ret i32 0
 ;
 entry:



More information about the llvm-commits mailing list