Bug type: crash
Component: LoopVectorize
Bug location: llvm/lib/Transforms/Vectorize/VPlan.h: ~VPExpressionRecipe
llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp: VPExpressionRecipe::VPExpressionRecipe
VPExpressionRecipe::decompose
Issue title: VPExpressionRecipe discards duplicate recipes.
Labels: vectorizers
crash-on-valid
Reported by: SamTebbs33

The `VPExpressionRecipe` class takes two extend recipes and a multiply recipe and bundles them into one so that the cost of the extends and multiply are hidden from the cost model. However, it uses a set to store these recipes and so any duplicates are discarded, meaning any attempt to use those recipes for cost computation or printing causes an error.

```
; opt -passes=loop-vectorize -force-vector-interleave=1 -force-vector-width=4 -prefer-inloop-reductions --debug-only=loop-vectorize
define i64 @print_mulacc_extended(ptr nocapture readonly %x, i32 %n) {
entry:
  br label %loop

loop:
  %iv = phi i32 [ %iv.next, %loop ], [ 0, %entry ]
  %rdx = phi i64 [ %rdx.next, %loop ], [ 0, %entry ]
  %arrayidx = getelementptr inbounds i16, ptr %x, i32 %iv
  %load0 = load i16, ptr %arrayidx, align 4
  %conv0 = sext i16 %load0 to i32
  %mul = mul nsw i32 %conv0, %conv0
  %conv = sext i32 %mul to i64
  %rdx.next = add nsw i64 %rdx, %conv
  %iv.next = add nuw nsw i32 %iv, 1
  %exitcond = icmp eq i32 %iv.next, %n
  br i1 %exitcond, label %exit, label %loop

exit:
  %r.0.lcssa = phi i64 [ %rdx.next, %loop ]
  ret i64 %r.0.lcssa
}
```
causes
```[...]
<x1> vector loop: {
  vector.body:
    EMIT vp<%4> = CANONICAL-INDUCTION ir<0>, vp<%index.next>
    WIDEN-REDUCTION-PHI ir<%rdx> = phi vp<%3>, vp<%7>
    vp<%5> = SCALAR-STEPS vp<%4>, ir<1>, vp<%0>
    CLONE ir<%arrayidx> = getelementptr inbounds ir<%x>, vp<%5>
    vp<%6> = vector-pointer ir<%arrayidx>
    WIDEN ir<%load0> = load vp<%6>
    EXPRESSION vp<%7> = ir<%rdx> + reduce.add (mulopt: /home/work/llvm-project/llvm/include/llvm/Support/Casting.h:578: decltype(auto) llvm::cast(From*) [with To = VPWidenRecipe; From = VPSingleDefRecipe]: Assertion `isa<To>(Val) && "cast<Ty>() argument of incompatible type!"' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace.
```
The recipe list should contain the two extends, the mul and the reduction, but since duplicates are discarded, it's only one extend, the mul and reduction. The printing code attempts to access the mul from `ExpressionRecipes[2]`, where it should be, but that is actually the reduction.

Comments: author: SamTebbs33
body: @fhahn FYI

Fix patch:
commit 664b227089a2d3a72b15018018c5e8e4e639f944
Author: Sam Tebbs <samuel.tebbs@arm.com>
Date:   Wed Oct 1 16:01:54 2025 +0100

    [LV] Keep duplicate recipes in VPExpressionRecipe (#156976)
    
    The VPExpressionRecipe class uses a set to store its bundled recipes. If
    repeated recipes are bundled then the duplicates will be lost, causing
    the following recipes to not be at the expected place in the set.
    
    When printing a reduce.add(mul(ext, ext)) bundle, for example, if the
    extends are the same then the 3rd element of the set will be the
    reduction, rather than the expected mul, causing a cast error. With this
    change, the recipes are at the expected index in the set.
    
    Fixes #156464

diff --git a/llvm/lib/Transforms/Vectorize/VPlan.h b/llvm/lib/Transforms/Vectorize/VPlan.h
index 10d704df289c..c167dd7f65fa 100644
--- a/llvm/lib/Transforms/Vectorize/VPlan.h
+++ b/llvm/lib/Transforms/Vectorize/VPlan.h
@@ -29,6 +29,7 @@
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/SmallBitVector.h"
 #include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/SmallSet.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/Twine.h"
 #include "llvm/ADT/ilist.h"
@@ -2977,7 +2978,8 @@ public:
 /// the expression is elevated to connect the non-expression recipe with the
 /// VPExpressionRecipe itself.
 class VPExpressionRecipe : public VPSingleDefRecipe {
-  /// Recipes included in this VPExpressionRecipe.
+  /// Recipes included in this VPExpressionRecipe. This could contain
+  /// duplicates.
   SmallVector<VPSingleDefRecipe *> ExpressionRecipes;
 
   /// Temporary VPValues used for external operands of the expression, i.e.
@@ -3039,8 +3041,11 @@ public:
   }
 
   ~VPExpressionRecipe() override {
-    for (auto *R : reverse(ExpressionRecipes))
-      delete R;
+    SmallPtrSet<VPSingleDefRecipe *, 4> ExpressionRecipesSeen;
+    for (auto *R : reverse(ExpressionRecipes)) {
+      if (ExpressionRecipesSeen.insert(R).second)
+        delete R;
+    }
     for (VPValue *T : LiveInPlaceholders)
       delete T;
   }
diff --git a/llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp b/llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp
index 3a55710d59b0..46909a53a954 100644
--- a/llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp
+++ b/llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp
@@ -2755,10 +2755,7 @@ VPExpressionRecipe::VPExpressionRecipe(
     ExpressionTypes ExpressionType,
     ArrayRef<VPSingleDefRecipe *> ExpressionRecipes)
     : VPSingleDefRecipe(VPDef::VPExpressionSC, {}, {}),
-      ExpressionRecipes(SetVector<VPSingleDefRecipe *>(
-                            ExpressionRecipes.begin(), ExpressionRecipes.end())
-                            .takeVector()),
-      ExpressionType(ExpressionType) {
+      ExpressionRecipes(ExpressionRecipes), ExpressionType(ExpressionType) {
   assert(!ExpressionRecipes.empty() && "Nothing to combine?");
   assert(
       none_of(ExpressionRecipes,
@@ -2802,14 +2799,22 @@ VPExpressionRecipe::VPExpressionRecipe(
         continue;
       addOperand(Op);
       LiveInPlaceholders.push_back(new VPValue());
-      R->setOperand(Idx, LiveInPlaceholders.back());
     }
   }
+
+  // Replace each external operand with the first one created for it in
+  // LiveInPlaceholders.
+  for (auto *R : ExpressionRecipes)
+    for (auto const &[LiveIn, Tmp] : zip(operands(), LiveInPlaceholders))
+      R->replaceUsesOfWith(LiveIn, Tmp);
 }
 
 void VPExpressionRecipe::decompose() {
   for (auto *R : ExpressionRecipes)
-    R->insertBefore(this);
+    // Since the list could contain duplicates, make sure the recipe hasn't
+    // already been inserted.
+    if (!R->getParent())
+      R->insertBefore(this);
 
   for (const auto &[Idx, Op] : enumerate(operands()))
     LiveInPlaceholders[Idx]->replaceAllUsesWith(Op);


Regression tests:
file: llvm/test/Transforms/LoopVectorize/reduction-inloop.ll
commands: opt < %s -passes=loop-vectorize -force-vector-interleave=1 -force-vector-width=4 -prefer-inloop-reductions -S
opt < %s -passes=loop-vectorize -force-vector-interleave=2 -force-vector-width=4 -prefer-inloop-reductions -S
tests: test_name: reduction_expression_same_operands
test_body: target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"

define i64 @reduction_expression_same_operands(ptr readonly captures(none) %x, ptr readonly captures(none) %y, i32 %n) {
entry:
  br label %loop

loop:                                             ; preds = %loop, %entry
  %iv = phi i32 [ %iv.next, %loop ], [ 0, %entry ]
  %rdx = phi i64 [ %rdx.next, %loop ], [ 0, %entry ]
  %arrayidx = getelementptr inbounds i16, ptr %x, i32 %iv
  %load0 = load i16, ptr %arrayidx, align 4
  %conv0 = sext i16 %load0 to i32
  %conv1 = sext i16 %load0 to i32
  %mul = mul nsw i32 %conv0, %conv1
  %conv = sext i32 %mul to i64
  %rdx.next = add nsw i64 %rdx, %conv
  %iv.next = add nuw nsw i32 %iv, 1
  %exitcond = icmp eq i32 %iv.next, %n
  br i1 %exitcond, label %exit, label %loop

exit:                                             ; preds = %loop
  %r.0.lcssa = phi i64 [ %rdx.next, %loop ]
  ret i64 %r.0.lcssa
}

file: llvm/test/Transforms/LoopVectorize/vplan-printing-reductions.ll
commands: opt -passes=loop-vectorize -debug-only=loop-vectorize -force-vector-interleave=1 -force-vector-width=4 -prefer-inloop-reductions -disable-output %s 2>&1
tests: test_name: print_mulacc_duplicate_extends
test_body: define i64 @print_mulacc_duplicate_extends(ptr readonly captures(none) %x, ptr readonly captures(none) %y, i32 %n) {
entry:
  br label %loop

loop:                                             ; preds = %loop, %entry
  %iv = phi i32 [ %iv.next, %loop ], [ 0, %entry ]
  %rdx = phi i64 [ %rdx.next, %loop ], [ 0, %entry ]
  %arrayidx = getelementptr inbounds i16, ptr %x, i32 %iv
  %load0 = load i16, ptr %arrayidx, align 4
  %conv0 = sext i16 %load0 to i32
  %mul = mul nsw i32 %conv0, %conv0
  %conv = sext i32 %mul to i64
  %rdx.next = sub nsw i64 %rdx, %conv
  %iv.next = add nuw nsw i32 %iv, 1
  %exitcond = icmp eq i32 %iv.next, %n
  br i1 %exitcond, label %exit, label %loop

exit:                                             ; preds = %loop
  %r.0.lcssa = phi i64 [ %rdx.next, %loop ]
  ret i64 %r.0.lcssa
}