Bug type: miscompilation
Component: SeparateConstOffsetFromGEP
Bug location: llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp: ConstantOffsetExtractor::applyExts
Issue title: separate-const-offset-from-gep miscompile related to "trunc nuw"
Labels: miscompilation
llvm:transforms
Reported by: mikaelholmen

llvm commit: 673750feea

Reproduce with:
```
opt -passes=separate-const-offset-from-gep bbi-109669_sep.ll -S -o - -mtriple=hexagon
```
The input program does
```
%0 = phi i32 [ -20, %entry ]
%1 = add i32 %0, 20
%2 = trunc nuw i32 %1 to i16
```
where the "trunc nuw" is ok since %1 is 0. No truncated bits are set.

However, in the result we get:
```
%0 = phi i32 [ -20, %entry ]
[...]
%1 = trunc nuw i32 %0 to i16
```
so here the input to "trunc nuw" is negative and the result is poison.
Langref https://llvm.org/docs/LangRef.html#trunc-to-instruction says
"If the nuw keyword is present, and any of the truncated bits are non-zero, the result is a [poison value](https://llvm.org/docs/LangRef.html#poisonvalues)."

Then %1 is further used to compute the return value from the function so the whole thing results in poison which it did not in the input.

(I stumbled upon this as a miscompile after
 https://github.com/llvm/llvm-project/pull/152990
which I first incorrectly blamed.)

[bbi-109669_sep.ll.gz](https://github.com/user-attachments/files/21836248/bbi-109669_sep.ll.gz)

Comments: author: bjope
body: I don't think I'm an expert at SeparateConstOffsetFromGEP, but some ideas after looking at the source code:
* One idea is to let `ConstantOffsetExtractor::applyExts `drop poison generating flags from any TruncInst when cloning the chain. Afaict distributing trunc over add/sub (and disjoint or) should be fine if we drop the flags.
* Another idea is to track if we have truncate with nuw/nsw in `ConstantOffsetExtractor::find` and `ConstantOffsetExtractor::CanTraceInto` to skip rewrites in scenarios when we can't keep `trunc nuw`.

But maybe there are other solutions?
author: nikic
body: cc @ritter-x2a @jrbyrnes @krzysz00 

> One idea is to let `ConstantOffsetExtractor::applyExts `drop poison generating flags from any TruncInst when cloning the chain. Afaict distributing trunc over add/sub (and disjoint or) should be fine if we drop the flags.

That sounds reasonable to me.
author: krzysz00
body: Yeah, dropping the `nsw` or `nuw` is probablyt he safe thing

(That or doing some analysis on the operands to the add to see if they're also small enough)
author: ritter-x2a
body: I think there is more broken around distributing truncs in separate-const-offset-from-gep. For example, I got this miscompilation while checking if the fix makes sense: https://alive2.llvm.org/ce/z/hLPfcw

It seems odd that `NonNegative` in `ConstantOffsetExtractor::find` [isn't cleared for trunc](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L606-L609) but [it is for zext](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L617-L620) "because zext(a) >= 0 does not imply a >= 0" -- that doesn't hold for trunc either.
The meaning of `NonNegative` also seems odd: [the doxygen comment](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L235-L239) says
> NonNegative -- Whether V is guaranteed to be non-negative. For example, an index of an inbounds GEP is guaranteed to be non-negative. Levaraging this, we can better split inbounds GEPs."

I don't think that's right, inbounds GEP indices can be negative.
author: bjope
body: > It seems odd that `NonNegative` in `ConstantOffsetExtractor::find` [isn't cleared for trunc](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L606-L609) but [it is for zext](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L617-L620) "because zext(a) >= 0 does not imply a >= 0" -- that doesn't hold for trunc either. 
 
I've been suspecting that there could be more problems in that area. Wondering if perhaps `NonNegative` should be cleared for trunc. And I'm not quite sure it is correct to just pass on `SignExtended` and `ZeroExtended` either for trunc.

I'm not sure if there is any comprehensive tests involving scenarios with combinarions of sext/zext/trunc/add/sub/or/xor. The support for xor is for example not using CanTraceInto, so unclear to me how that works together with `NonNegative`, `SignExtended` and `ZeroExtended`.

Still think dropping the nuw/nsw flags (https://github.com/llvm/llvm-project/pull/154582) would solve the problem given the IR reproducer in this issue. But agree that more problems might be lurking.
author: mikaelholmen
body: > I think there is more broken around distributing truncs in separate-const-offset-from-gep. For example, I got this miscompilation while checking if the fix makes sense: https://alive2.llvm.org/ce/z/hLPfcw
> 
> It seems odd that `NonNegative` in `ConstantOffsetExtractor::find` [isn't cleared for trunc](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L606-L609) but [it is for zext](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L617-L620) "because zext(a) >= 0 does not imply a >= 0" -- that doesn't hold for trunc either. The meaning of `NonNegative` also seems odd: [the doxygen comment](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L235-L239) says
> 
> > NonNegative -- Whether V is guaranteed to be non-negative. For example, an index of an inbounds GEP is guaranteed to be non-negative. Levaraging this, we can better split inbounds GEPs."
> 
> I don't think that's right, inbounds GEP indices can be negative.

@ritter-x2a : 
Will you write a new issue about the miscompile you found or how do we make sure that is not just forgotten?
author: ritter-x2a
body: > > I think there is more broken around distributing truncs in separate-const-offset-from-gep. For example, I got this miscompilation while checking if the fix makes sense: https://alive2.llvm.org/ce/z/hLPfcw
> > It seems odd that `NonNegative` in `ConstantOffsetExtractor::find` [isn't cleared for trunc](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L606-L609) but [it is for zext](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L617-L620) "because zext(a) >= 0 does not imply a >= 0" -- that doesn't hold for trunc either. The meaning of `NonNegative` also seems odd: [the doxygen comment](https://github.com/llvm/llvm-project/blob/f306e0aeb2c72e040c59e160e88af3bf76457693/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp#L235-L239) says
> > > NonNegative -- Whether V is guaranteed to be non-negative. For example, an index of an inbounds GEP is guaranteed to be non-negative. Levaraging this, we can better split inbounds GEPs."
> > 
> > 
> > I don't think that's right, inbounds GEP indices can be negative.
> 
> [@ritter-x2a](https://github.com/ritter-x2a) : Will you write a new issue about the miscompile you found or how do we make sure that is not just forgotten?

Here it is: #154936
I might be able to look into this myself in a few weeks, but right now I don't have the capacity.

Fix patch:
commit 2d3167f8d86ca9bdaff44cf839488c5a513f5583
Author: Bjorn Pettersson <bjorn.a.pettersson@ericsson.com>
Date:   Wed Aug 20 19:26:11 2025 +0200

    [SeparateConstOffsetFromGEP] Avoid miscompiles related to trunc nuw/nsw (#154582)
    
    Drop poison generating flags on trunc when distributing trunc over
    add/sub/or. We need to do this since for example
    (add (trunc nuw A), (trunc nuw B)) is more poisonous than
    (trunc nuw (add A, B))).
    
    In some situations it is pessimistic to drop the flags. Such as
    if the add in the example above also has the nuw flag. For now we
    keep it simple and always drop the flags.
    
    Worth mentioning is that we drop the flags when cloning
    instructions and rebuilding the chain. This is done after the
    "allowsPreservingNUW" checks in ConstantOffsetExtractor::Extract.
    So we still take the "trunc nuw" into consideration when determining
    if nuw can be preserved in the gep (which should be ok since that
    check also require that all the involved binary operations has nuw).
    
    Fixes #154116

diff --git a/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp b/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp
index a88f8f19f852..8b9d06d7e443 100644
--- a/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp
+++ b/llvm/lib/Transforms/Scalar/SeparateConstOffsetFromGEP.cpp
@@ -642,6 +642,13 @@ Value *ConstantOffsetExtractor::applyExts(Value *V) {
 
     Instruction *Ext = I->clone();
     Ext->setOperand(0, Current);
+    // In ConstantOffsetExtractor::find we do not analyze nuw/nsw for trunc, so
+    // we assume that it is ok to redistribute trunc over add/sub/or. But for
+    // example (add (trunc nuw A), (trunc nuw B)) is more poisonous than (trunc
+    // nuw (add A, B))). To make such redistributions legal we drop all the
+    // poison generating flags from cloned trunc instructions here.
+    if (isa<TruncInst>(Ext))
+      Ext->dropPoisonGeneratingFlags();
     Ext->insertBefore(*IP->getParent(), IP);
     Current = Ext;
   }


Regression tests:
file: llvm/test/Transforms/SeparateConstOffsetFromGEP/AMDGPU/preserve-inbounds.ll
commands: opt < %s -mtriple=amdgcn-amd-amdhsa -mcpu=gfx1200 -passes=separate-const-offset-from-gep -S
tests: test_name: nuw_inbounds_implies_nuw_inbounds_trunc_nuw
test_body: define ptr @nuw_inbounds_implies_nuw_inbounds_trunc_nuw(ptr %p, i128 %i) {
entry:
  %idx = add nuw i128 %i, 1
  %idx.conv = trunc nuw i128 %idx to i64
  %arrayidx = getelementptr inbounds nuw i32, ptr %p, i64 %idx.conv
  ret ptr %arrayidx
}

file: llvm/test/Transforms/SeparateConstOffsetFromGEP/AMDGPU/rebuild-trunc.ll
commands: opt < %s -mtriple=x86_64-- -passes=separate-const-offset-from-gep -S
tests: test_name: pr154116_nsw
test_body: define ptr @pr154116_nsw(ptr %p, i128 %i) {
  %idx = add i128 %i, 1
  %idx.conv = trunc nsw i128 %idx to i64
  %arrayidx = getelementptr i32, ptr %p, i64 %idx.conv
  ret ptr %arrayidx
}

test_name: pr154116_nuw
test_body: define ptr @pr154116_nuw(ptr %p, i128 %i) {
  %idx = add i128 %i, 20
  %idx.conv = trunc nuw i128 %idx to i64
  %arrayidx = getelementptr i32, ptr %p, i64 %idx.conv
  ret ptr %arrayidx
}