Bug type: crash
Component: LoopFuse
Bug location: llvm/lib/Transforms/Scalar/LoopFuse.cpp: isStrictlyAdjacent
Issue title: [LoopFusion] Assert "Expecting guard block to have no predecessors"
Labels: loopoptim
crash-on-valid
loop-fusion
Reported by: sjoerdmeijer

This IR:

```
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32"
target triple = "aarch64-unknown-linux-gnu"

define void @_Z1gb() {
entry:
  br i1 false, label %if.else.1, label %for.cond.cleanup13

for.cond.cleanup13:                               ; preds = %for.cond.cleanup13, %entry
  br i1 true, label %for.inc21, label %for.cond.cleanup13

for.inc21:                                        ; preds = %for.cond.cleanup13
  br label %if.then.1

if.then.1:                                        ; preds = %for.inc21
  br label %if.else.1

if.else.1:                                        ; preds = %if.then.1, %entry
  br i1 false, label %if.else.2, label %for.cond.cleanup13.1

for.cond.cleanup13.1:                             ; preds = %for.cond.cleanup13.1, %if.else.1
  br i1 true, label %if.else.2, label %for.cond.cleanup13.1

if.else.2:                                        ; preds = %for.cond.cleanup13.1, %if.else.1
  ret void
}
```

triggers this assert:

```
opt: /root/llvm-project/llvm/lib/Transforms/Scalar/LoopFuse.cpp:1945: llvm::Loop* {anonymous}::LoopFuser::fuseGuardedLoops(const {anonymous}::FusionCandidate&, const {anonymous}::FusionCandidate&): Assertion `pred_empty(FC1GuardBlock) && "Expecting guard block to have no predecessors"' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace and instructions to reproduce the bug.
Stack dump:
0.	Program arguments: /opt/compiler-explorer/clang-assertions-trunk/bin/opt -o /app/output.s -S -S -passes=loop-fusion <source>
1.	Running pass "function(loop-fusion)" on module "<source>"
2.	Running pass "loop-fusion" on function "_Z1gb"
 #0 0x0000000005989ef8 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/opt/compiler-explorer/clang-assertions-trunk/bin/opt+0x5989ef8)
 #1 0x0000000005986da4 SignalHandler(int, siginfo_t*, void*) Signals.cpp:0:0
 #2 0x00007f6389e42520 (/lib/x86_64-linux-gnu/libc.so.6+0x42520)
 #3 0x00007f6389e969fc pthread_kill (/lib/x86_64-linux-gnu/libc.so.6+0x969fc)
 #4 0x00007f6389e42476 gsignal (/lib/x86_64-linux-gnu/libc.so.6+0x42476)
 #5 0x00007f6389e287f3 abort (/lib/x86_64-linux-gnu/libc.so.6+0x287f3)
 #6 0x00007f6389e2871b (/lib/x86_64-linux-gnu/libc.so.6+0x2871b)
 #7 0x00007f6389e39e96 (/lib/x86_64-linux-gnu/libc.so.6+0x39e96)
 #8 0x00000000046a310b (anonymous namespace)::LoopFuser::fuseGuardedLoops((anonymous namespace)::FusionCandidate const&, (anonymous namespace)::FusionCandidate const&) LoopFuse.cpp:0:0
 #9 0x00000000046a329a (anonymous namespace)::LoopFuser::performFusion((anonymous namespace)::FusionCandidate const&, (anonymous namespace)::FusionCandidate const&) LoopFuse.cpp:0:0
#10 0x00000000046a938e (anonymous namespace)::LoopFuser::fuseCandidates() LoopFuse.cpp:0:0
#11 0x00000000046aaef1 llvm::LoopFusePass::run(llvm::Function&, llvm::AnalysisManager<llvm::Function>&) (/opt/compiler-explorer/clang-assertions-trunk/bin/opt+0x46aaef1)
```

when compiled with `-S -passes=loop-fusion`, see also: https://godbolt.org/z/v9ddvW1T6


The IR reproducer comes from this C reproducer:

```
int a;
short b;
char c, e, f;
long d;
void g(bool h) {
  for (int i; i < 20ULL; i += 4ULL)
    if (h)
      if (c)
        a = f;
      else if (e)
        for (int j(d); j; j++)
          for (int k(f); k; k += 4073709530269)
            b = 0;
}
```

Comments: author: amehsan
body: @1997alireza could you look into this issue?
author: 1997alireza
body: > [@1997alireza](https://github.com/1997alireza) could you look into this issue?

Yes. On it.
author: madhur13490
body: Hi @1997alireza, since you're looking at it, I have assigned it to you. I am using the filter below to manage all loop-fusion crashes. 
https://github.com/llvm/llvm-project/issues?q=state%3Aopen%20label%3A%22loop-fusion%22
author: amehsan
body: The issue is the following: LoopFusion implicitly expects that if `FC0` is not peeled then the successor of exit block of `FC0` is the same as entry block of `FC1`. This is not true for the test case above, 

```
for.inc21:                           <------------- EXIT BLOCK OF FC0
  br label %if.then.1

if.then.1:                                    <---------- INTERVENING BLOCK THAT LOOP FUSION IS UNAWARE OF  
  br label %if.else.1

if.else.1:                                        <------------- ENTRY BLOCK OF FC1
  br i1 false, label %if.else.2, label %for.cond.cleanup13.1
```


The testcase passes `isAdjacent` because loop guard of `FC0` branches to entry block of `FC1` 

```
entry:   <------------------ LOOP GUARD OF FC0
  br i1 false, label %if.else.1, label %for.cond.cleanup13
```

This is the relevant code in `isAdjacent`

```
    if (FC0.GuardBranch)
      return FC0.getNonLoopBlock() == FC1.getEntryBlock();
```


However later we have this code:
```
    BasicBlock *BBToUpdate = FC0.Peeled ? FC0ExitBlockSuccessor : FC0.ExitBlock;
    BBToUpdate->getTerminator()->replaceUsesOfWith(FC1GuardBlock, FC1.Header);
```

So if `FC0` is not peeled we replace all uses of `FC1GuardBlock`  with `FC1.Header` in `FC0.ExitBlock`. In our case terminator of `FC0.ExitBlock` has no use of `FC1GuardBlock` . So our expectation that one of the predecessors of `FC1GuardBlock`  will be removed at this point is not satisfied and at the time of assertion the block has an unexpected predecessor.

Looks like we have a missing test in legality check (possibly in `isAdjacent`). But a little more investigation is needed to figure out how this interacts with peeling


author: 1997alireza
body: The issue is fixed by PR #175034. I close the issue.

Fix patch:
commit 5ab966aacbd8bbe95bf858110569a8205b7f8c79
Author: Alireza Torabian <alireza.torabian@huawei.com>
Date:   Thu Jan 8 17:07:15 2026 -0500

    [LoopFusion] Non-loop block must be the immediate successor of exit (#175034)
    
    Loop fusion assumes the non-loop block of a guarded adjacent loop is the
    immediate successor of its exit block. This patch ensures this condition
    is hold and fixes the crash #166356.

diff --git a/llvm/lib/Transforms/Scalar/LoopFuse.cpp b/llvm/lib/Transforms/Scalar/LoopFuse.cpp
index 3a06c3f00fa0..d7284c98b48c 100644
--- a/llvm/lib/Transforms/Scalar/LoopFuse.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopFuse.cpp
@@ -1399,10 +1399,11 @@ private:
   /// This method will determine if there are additional basic blocks in the CFG
   /// between the exit of \p FC0 and the entry of \p FC1.
   /// If the two candidates are guarded loops, then it checks whether the
-  /// non-loop successor of the \p FC0 guard branch is the entry block of \p
-  /// FC1. If not, then the loops are not adjacent. If the two candidates are
-  /// not guarded loops, then it checks whether the exit block of \p FC0 is the
-  /// preheader of \p FC1.
+  /// exit block of the \p FC0 is the predecessor of the \p FC1 preheader. This
+  /// implicitly ensures that the non-loop successor of the \p FC0 guard branch
+  /// is the entry block of \p FC1. If not, then the loops are not adjacent. If
+  /// the two candidates are not guarded loops, then it checks whether the exit
+  /// block of \p FC0 is the preheader of \p FC1.
   /// Strictly means there is no predecessor for FC1 unless it is from FC0,
   /// i.e., FC0 dominates FC1.
   bool isStrictlyAdjacent(const FusionCandidate &FC0,
@@ -1410,7 +1411,7 @@ private:
     // If the successor of the guard branch is FC1, then the loops are adjacent
     if (FC0.GuardBranch)
       return DT.dominates(FC0.getEntryBlock(), FC1.getEntryBlock()) &&
-             FC0.getNonLoopBlock() == FC1.getEntryBlock();
+             FC0.ExitBlock->getSingleSuccessor() == FC1.getEntryBlock();
     else
       return FC0.ExitBlock == FC1.getEntryBlock();
   }


Regression tests:
file: llvm/test/Transforms/LoopFusion/pr166356.ll
commands: opt -passes=loop-fusion -S < %s 2>&1
tests: test_name: <module>
test_body: 
; The non-loop block of loop for.cond.cleanup13 is not the immediate successor
; of its exit block, then it should not be eligible for fusion.

define void @non_immediate_exit() {
;
entry:
  br i1 false, label %if.else.1, label %for.cond.cleanup13

for.cond.cleanup13:                               ; preds = %for.cond.cleanup13, %entry
  br i1 true, label %for.inc21, label %for.cond.cleanup13

for.inc21:                                        ; preds = %for.cond.cleanup13
  br label %if.then.1

if.then.1:                                        ; preds = %for.inc21
  br label %if.else.1

if.else.1:                                        ; preds = %if.then.1, %entry
  br i1 false, label %if.else.2, label %for.cond.cleanup13.1

for.cond.cleanup13.1:                             ; preds = %for.cond.cleanup13.1, %if.else.1
  br i1 true, label %if.else.2, label %for.cond.cleanup13.1

if.else.2:                                        ; preds = %for.cond.cleanup13.1, %if.else.1
  ret void
}