Bug type: miscompilation
Component: ConstraintElimination
Bug location: llvm/lib/Transforms/Scalar/ConstraintElimination.cpp: State::addInfoFor
Issue title: [ConstraintElimination] A miscompile in presence of `shl nuw nsw` and `llvm.umin`
Labels: miscompilation
release:backport
llvm:transforms
Reported by: DaniilSuchkov

The following commit: https://github.com/llvm/llvm-project/commit/71f56e49ceca75dbf82cbb9537c2545c2d2e51c9
Triggered this miscompile: https://alive2.llvm.org/ce/z/oBg-u8

Now `opt -passes=constraint-elimination -S` turns
```
define i1 @test(i32 %arg) {
  %icmp = icmp slt i32 %arg, 0
  %shl = shl nuw nsw i32 %arg, 3
  %call4 = call i32 @llvm.umin.i32(i32 %shl, i32 80)
  ret i1 %icmp
}

declare i32 @llvm.umin.i32(i32, i32) #0

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
```
into
```
define i1 @test(i32 %arg) {
  %shl = shl nuw nsw i32 %arg, 3
  %call4 = call i32 @llvm.umin.i32(i32 %shl, i32 80)
  ret i1 false
}

declare i32 @llvm.umin.i32(i32, i32) #0

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
```

Given this debug output:
```
opt -passes=constraint-elimination before-constr-elim-renamed.ll -S -debug-only=constraint-elimination,constraint-system
Processing fact to add to the system: icmp ule i32 %call4, %shl
Adding 'icmp ule i32 %call4, %shl'
  constraint: -8 * %arg + %call4 <= 0

---
-1 * %arg <= 0
-8 * %arg + %call4 <= 0
-1 * %call4 <= 0
8 * %arg <= -1
unsat
Adding 'icmp sge i32 %call4, 0'
  constraint: -1 * %call4 <= 0

Adding 'icmp sle i32 %call4, %shl'
  constraint: -8 * %arg + %call4 <= 0

Processing fact to add to the system: icmp ule i32 %call4, 80
Adding 'icmp ule i32 %call4, 80'
  constraint: %call4 <= 80

Adding 'icmp sge i32 %call4, 0'
  constraint: -1 * %call4 <= 0

Adding 'icmp sle i32 %call4, 80'
  constraint: %call4 <= 80

Top of stack : 0 1
CB: 0 1
Processing condition to simplify:   %icmp = icmp slt i32 %arg, 0
Checking   %icmp = icmp slt i32 %arg, 0
---
-1 * %call4 <= 0
-8 * %arg + %call4 <= 0
-1 * %call4 <= 0
%call4 <= 80
-1 * %arg <= 0
sat
---
-1 * %call4 <= 0
-8 * %arg + %call4 <= 0
-1 * %call4 <= 0
%call4 <= 80
%arg <= -1
unsat
Condition icmp sge i32 %arg, 0 implied by dominating constraints
-1 * %call4 <= 0
-8 * %arg + %call4 <= 0
-1 * %call4 <= 0
%call4 <= 80
```
I'm not 100% sure if the aforementioned patch causes the miscompile, of if it merely triggers it on this specific reproducer. The fact that something like `-1 * %call4 <= 0` is reported as a "dominating constraint" seems suspicious to a person who's not familiar with the logic of ConstraintElimination and it doesn't look directly related to that patch. However, if I replace the `shl nuw nsw i32 %arg, 3
` with `mul nuw nsw i32 %arg, 8`, the miscompile doesn't happen: https://alive2.llvm.org/ce/z/EL2bCh.

Comments: author: nikic
body: From the debug log, I suspect that we're using the unsigned constraint system to prove a signed fact for some reason.

cc @fhahn 
author: nikic
body: Okay, this is an interesting case.

We are adding an `icmp ule i32 %call4, %shl` fact, which get transferred as `icmp sge i32 %call4, 0` and `icmp sle i32 %call4, %shl` to the signed system, because `%shl` is known non-negative.

Then we are checking whether a solution of these constraints plus `%arg <= -1` exists, which it does not.

This is "correct" in the sense that *if* that umin call returns a non-poison result, then `icmp slt i32 %arg, 0` is indeed known to be false. But we obviously can't actually make that assumption.

@dtcxzyw @fhahn This makes me think that the way we are currently adding facts for MinMaxIntrinsic (and I guess abs as well) is pretty fundamentally incorrect, because it effectively imposes a constraint that the umin operation returns a well-defined value. Any thoughts on how to fix this without dropping support for them entirely?
author: nikic
body: (The difference between shl and mul in the reproducer is because ValueTracking apparently currently doesn't know that mul nuw nsw is non-negative, but knows that shl nuw nsw is.)
author: dtcxzyw
body: > @dtcxzyw @fhahn This makes me think that the way we are currently adding facts for MinMaxIntrinsic (and I guess abs as well) is pretty fundamentally incorrect, because it effectively imposes a constraint that the umin operation returns a well-defined value. Any thoughts on how to fix this without dropping support for them entirely?

Can we conservatively check these intrinsics using `isGuaranteedNotToBePoison`?

author: nikic
body: If it's not too complicated, I think a better solution would be to only add the intrinsic constraints when the result variable is added to the constraint system.
author: fhahn
body: > If it's not too complicated, I think a better solution would be to only add the intrinsic constraints when the result variable is added to the constraint system.

One way to do that would be to check the uses of the intrinsic and find all the branches that it feeds, and add it as fact for the successors.
author: nikic
body: @dtcxzyw Do you plan to submit a PR for this issue?
author: dtcxzyw
body: > @dtcxzyw Do you plan to submit a PR for this issue?

I will post a patch later.
author: fhahn
body: I've pushed a fix that only adds the facts if the result is guaranteed to not be poison so the mis-compile is fixed. I also added a TODO to improve things if possible.

Reopening so we can pick the fix for the release branch
author: nikic
body: @fhahn Why does your fix only do this for min/max but not abs? Can't it have the same problem?
author: fhahn
body: > @fhahn Why does your fix only do this for min/max but not abs? Can't it have the same problem?

I wasn't able to come up with a problematic test case so far for `abs`, but there's a few more things to try.
author: DaniilSuchkov
body: @fhahn I'm not familiar with how this pass works, so I have a question: will this pass still use min/max/etc. to infer facts about the arguments of those intrinsics? I'm just a bit confused by the terminology: I see that it adds "constraints" based on things like `icmp`, which on their own don't actually constrain anything unless you branch on the result (or do something similar).
author: nikic
body: > @fhahn I'm not familiar with how this pass works, so I have a question: will this pass still use min/max/etc. to infer facts about the arguments of those intrinsics? I'm just a bit confused by the terminology: I see that it adds "constraints" based on things like `icmp`, which on their own don't actually constrain anything unless you branch on the result (or do something similar).

Constraints for icmps will only be added in branches that the icmp dominates (or code that an assume with an icmp dominates). The intrinsic handling was a bit special in that constraints are added unconditionally.

Fix patch:
commit 3d91d9613e294b242d853039209b40a0cb7853f2
Author: Florian Hahn <flo@fhahn.com>
Date:   Wed Jan 24 14:25:54 2024 +0000

    [ConstraintElim] Make sure min/max intrinsic results are not poison.
    
    The result of umin may be poison and in that case the added constraints
    are not be valid in contexts where poison doesn't cause UB. Only queue
    facts for min/max intrinsics if the result is guaranteed to not be
    poison.
    
    This could be improved in the future, by only adding the fact when
    solving conditions using the result value.
    
    Fixes https://github.com/llvm/llvm-project/issues/78621.

diff --git a/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp b/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
index 8f09569d0d9c..7b672e89b67a 100644
--- a/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
@@ -1061,11 +1061,16 @@ void State::addInfoFor(BasicBlock &BB) {
           FactOrCheck::getCheck(DT.getNode(&BB), cast<CallInst>(&I)));
       break;
     // Enqueue the intrinsics to add extra info.
-    case Intrinsic::abs:
     case Intrinsic::umin:
     case Intrinsic::umax:
     case Intrinsic::smin:
     case Intrinsic::smax:
+      // TODO: Check if it is possible to instead only added the min/max facts
+      // when simplifying uses of the min/max intrinsics.
+      if (!isGuaranteedNotToBePoison(&I))
+        break;
+      [[fallthrough]];
+    case Intrinsic::abs:
       WorkList.push_back(FactOrCheck::getInstFact(DT.getNode(&BB), &I));
       break;
     }


Regression tests:
file: llvm/test/Transforms/ConstraintElimination/minmax.ll
commands: opt -passes=constraint-elimination -S %s
tests: test_name: smin_branchless
test_body: define i1 @smin_branchless(i32 %x, i32 %y) {
entry:
  %min = call i32 @llvm.smin.i32(i32 %x, i32 %y)
  %cmp1 = icmp sle i32 %min, %x
  %cmp2 = icmp sgt i32 %min, %x
  %ret = xor i1 %cmp1, %cmp2
  ret i1 %ret
}

; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none)
declare i32 @llvm.smin.i32(i32, i32) #0

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }

file: llvm/test/Transforms/ConstraintElimination/umin-result-may-be-poison.ll
commands: opt -p constraint-elimination -S %s
tests: test_name: umin_poison_call_before_UB
test_body: define i1 @umin_poison_call_before_UB(i32 %arg) {
  %icmp = icmp slt i32 %arg, 0
  %shl = shl nuw nsw i32 %arg, 3
  %min = call i32 @llvm.umin.i32(i32 %shl, i32 80)
  call void @fn()
  call void @noundef(i32 noundef %min)
  %cmp2 = shl nuw nsw i32 %arg, 3
  ret i1 %icmp
}

; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none)
declare i32 @llvm.umin.i32(i32, i32) #0

declare void @noundef(i32 noundef)

declare void @fn()

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }

test_name: umin_not_used
test_body: define i1 @umin_not_used(i32 %arg) {
  %icmp = icmp slt i32 %arg, 0
  %shl = shl nuw nsw i32 %arg, 3
  %1 = call i32 @llvm.umin.i32(i32 %shl, i32 80)
  %cmp2 = shl nuw nsw i32 %arg, 3
  ret i1 %icmp
}

; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none)
declare i32 @llvm.umin.i32(i32, i32) #0

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }

test_name: umin_poison_is_UB_via_call
test_body: define i1 @umin_poison_is_UB_via_call(i32 %arg) {
  %icmp = icmp slt i32 %arg, 0
  %shl = shl nuw nsw i32 %arg, 3
  %min = call i32 @llvm.umin.i32(i32 %shl, i32 80)
  call void @noundef(i32 noundef %min)
  %cmp2 = shl nuw nsw i32 %arg, 3
  ret i1 %icmp
}

; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none)
declare i32 @llvm.umin.i32(i32, i32) #0

declare void @noundef(i32 noundef)

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }