Bug type: crash
Component: LoopVectorize
Bug location: llvm/lib/Transforms/Vectorize/VPlanUtils.cpp: vputils::getSCEVExprForVPValue
vputils::isHeaderMask
Issue title: [LV]VPlan cost model and legacy cost model disagreed
Labels: vectorizers
crash-on-valid
Reported by: alexey-bataev

https://godbolt.org/z/Gr8nEMWK9

Looks like it started crashing after `Fri Dec 26 13:38:14 2025  | LLVM   | 60e5b86052ca1103499e87ee32c8228c71b6c753 | [VPlan] Support extends and truncs in getSCEVExprForVPValue. (NFCI)`
@fhahn 

Comments: author: alexey-bataev
body: Ping
author: fhahn
body: Thanks will take a look

Fix patch:
commit 2f7e218017db69454ea84e1849fcb9a11b1f7fef
Author: Florian Hahn <flo@fhahn.com>
Date:   Sun Jan 11 20:51:12 2026 +0000

    [VPlan] Add missing sext(sub) SCEV fold to getSCEVExprForVPValue.
    
    SCEV has a manual fold when doing SCEV construction from IR, that is not
    integrated in the regular SCEV construction functions. Mirror the
    behavior in getSCEVExprForVPValue, to match results when constructing
    SCEVs from IR.
    
    Fixes https://github.com/llvm/llvm-project/issues/174622.

diff --git a/llvm/lib/Transforms/Vectorize/VPlanUtils.cpp b/llvm/lib/Transforms/Vectorize/VPlanUtils.cpp
index ae36df06bb81..09b77297a151 100644
--- a/llvm/lib/Transforms/Vectorize/VPlanUtils.cpp
+++ b/llvm/lib/Transforms/Vectorize/VPlanUtils.cpp
@@ -82,6 +82,61 @@ bool vputils::isHeaderMask(const VPValue *V, const VPlan &Plan) {
          B == Plan.getBackedgeTakenCount();
 }
 
+/// Returns true if \p R propagates poison from any operand to its result.
+static bool propagatesPoisonFromRecipeOp(const VPRecipeBase *R) {
+  return TypeSwitch<const VPRecipeBase *, bool>(R)
+      .Case<VPWidenGEPRecipe, VPWidenCastRecipe>(
+          [](const VPRecipeBase *) { return true; })
+      .Case<VPReplicateRecipe>([](const VPReplicateRecipe *Rep) {
+        // GEP and casts propagate poison from all operands.
+        unsigned Opcode = Rep->getOpcode();
+        return Opcode == Instruction::GetElementPtr ||
+               Instruction::isCast(Opcode);
+      })
+      .Default([](const VPRecipeBase *) { return false; });
+}
+
+/// Returns true if \p V being poison is guaranteed to trigger UB because it
+/// propagates to the address of a memory recipe.
+static bool poisonGuaranteesUB(const VPValue *V) {
+  SmallPtrSet<const VPValue *, 8> Visited;
+  SmallVector<const VPValue *, 16> Worklist;
+
+  Worklist.push_back(V);
+
+  while (!Worklist.empty()) {
+    const VPValue *Current = Worklist.pop_back_val();
+    if (!Visited.insert(Current).second)
+      continue;
+
+    for (VPUser *U : Current->users()) {
+      // Check if Current is used as an address operand for load/store.
+      if (auto *MemR = dyn_cast<VPWidenMemoryRecipe>(U)) {
+        if (MemR->getAddr() == Current)
+          return true;
+        continue;
+      }
+      if (auto *Rep = dyn_cast<VPReplicateRecipe>(U)) {
+        unsigned Opcode = Rep->getOpcode();
+        if ((Opcode == Instruction::Load && Rep->getOperand(0) == Current) ||
+            (Opcode == Instruction::Store && Rep->getOperand(1) == Current))
+          return true;
+      }
+
+      // Check if poison propagates through this recipe to any of its users.
+      auto *R = cast<VPRecipeBase>(U);
+      for (const VPValue *Op : R->operands()) {
+        if (Op == Current && propagatesPoisonFromRecipeOp(R)) {
+          Worklist.push_back(R->getVPSingleValue());
+          break;
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
 const SCEV *vputils::getSCEVExprForVPValue(const VPValue *V,
                                            PredicatedScalarEvolution &PSE,
                                            const Loop *L) {
@@ -140,6 +195,20 @@ const SCEV *vputils::getSCEVExprForVPValue(const VPValue *V,
   if (match(V, m_SExt(m_VPValue(LHSVal)))) {
     const VPlan *Plan = V->getDefiningRecipe()->getParent()->getPlan();
     Type *DestTy = VPTypeAnalysis(*Plan).inferScalarType(V);
+
+    // Mirror SCEV's createSCEV handling for sext(sub nsw): push sign extension
+    // onto the operands before computing the subtraction.
+    VPValue *SubLHS, *SubRHS;
+    auto *SubR = dyn_cast<VPRecipeWithIRFlags>(LHSVal);
+    if (match(LHSVal, m_Sub(m_VPValue(SubLHS), m_VPValue(SubRHS))) && SubR &&
+        SubR->hasNoSignedWrap() && poisonGuaranteesUB(LHSVal)) {
+      const SCEV *V1 = getSCEVExprForVPValue(SubLHS, PSE, L);
+      const SCEV *V2 = getSCEVExprForVPValue(SubRHS, PSE, L);
+      if (!isa<SCEVCouldNotCompute>(V1) && !isa<SCEVCouldNotCompute>(V2))
+        return SE.getMinusSCEV(SE.getSignExtendExpr(V1, DestTy),
+                               SE.getSignExtendExpr(V2, DestTy), SCEV::FlagNSW);
+    }
+
     return CreateSCEV({LHSVal}, [&](ArrayRef<const SCEV *> Ops) {
       return SE.getSignExtendExpr(Ops[0], DestTy);
     });


Regression tests:
file: llvm/test/Transforms/LoopVectorize/AArch64/induction-costs.ll
commands: opt -p loop-vectorize -mtriple=arm64-apple-macosx -S %s
tests: test_name: sext_sub_nsw_for_address
test_body: target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128"

define void @sext_sub_nsw_for_address(ptr %base, i64 %n, ptr %src) #0 {
entry:
  br label %loop

loop:                                             ; preds = %loop, %entry
  %iv = phi i64 [ 0, %entry ], [ %iv.next, %loop ]
  %count = phi i64 [ %n, %entry ], [ %count.next, %loop ]
  %iv.i32 = phi i32 [ 0, %entry ], [ %iv.i32.next, %loop ]
  %gep.1 = getelementptr double, ptr %src, i64 %iv
  %gep.2 = getelementptr i8, ptr %gep.1, i64 -8
  store double 0.000000e+00, ptr %gep.2, align 8
  %neg = sub nsw i32 0, %iv.i32
  %neg.ext = sext i32 %neg to i64
  %gep.3 = getelementptr double, ptr %base, i64 %neg.ext
  %l = load double, ptr %gep.3, align 8
  store double %l, ptr %gep.1, align 8
  %iv.i32.next = add i32 %iv.i32, 2
  %iv.next = add i64 %iv, 2
  %count.next = add i64 %count, -1
  %ec = icmp sgt i64 %count, 0
  br i1 %ec, label %loop, label %exit

exit:                                             ; preds = %loop
  ret void
}

attributes #0 = { "target-cpu"="neoverse-v2" }