llvm / llvm/llvm-project

[DebugInfo][InstCombine] fcmp one branch canonicalization leaves an IR-level dbg_value describing the inverted predicate

Open
#215,183 3 comments 0 reactions 0 assignees View on GitHub
confirmed debuginfo llvm:instcombine
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Description

This is an IR-level debug-info issue observed directly in `.ll`. When `instcombine`
canonicalizes a branch on `fcmp one`, it replaces the compare with `fcmp ueq` and
swaps the branch successors to preserve the optimized program behavior.

However, the debug value for the source variable `C` is still attached directly to the
new `%cmp`:

```llvm
#dbg_value(i1 %cmp, !19, !DIExpression(...), !16)
```

Before `instcombine`, `%cmp` is the source value of `C`:

```llvm
%cmp = fcmp one float %X, %Y
```

After `instcombine`, `%cmp` has the opposite meaning:

```llvm
%cmp = fcmp ueq float %X, %Y
```

So the optimized IR preserves the control-flow behavior by swapping branch targets,
but the debug value for `C` is no longer updated to preserve the source-level value.

This report is specifically about the `fcmp one` predicate. For the testcase below,
`C` is the result of `__builtin_islessgreater(X, Y)`, which lowers to `fcmp one`.

## Reproducer

`case.c`:

```c
__attribute__((noinline))
int f_one(float X, float Y) {
int C = __builtin_islessgreater(X, Y);
if (__builtin_expect(C, 0))
return 12;
else
return 123;
}

int main() {
f_one(1.0, 2.0);
return 0;
}
```

Build pipeline:

```sh
clang -g -O0 -Xclang -disable-O0-optnone -fno-discard-value-names -S -emit-llvm case.c -o case.ll
opt -passes="mem2reg,instsimplify" -S case.ll -o src.ll
opt -passes=instcombine -S src.ll -o tgt.ll
```

## IR Generation Script

```sh
#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "$0")"

clang -g -O0 -Xclang -disable-O0-optnone -fno-discard-value-names -S -emit-llvm case.c -o case.ll
opt -passes="mem2reg,instsimplify" -S case.ll -o src.ll
opt -passes=instcombine -S src.ll -o tgt.ll
```

## Observed Directly in `.ll`

Before `instcombine`, the `fcmp one` result is used both as the branch condition and
as the debug value for `C`:

```llvm
define dso_local i32 @f_one(float noundef %X, float noundef %Y) #0 !dbg !9 {
entry:
#dbg_value(float %X, !15, !DIExpression(), !16)
#dbg_value(float %Y, !17, !DIExpression(), !16)
%cmp = fcmp one float %X, %Y, !dbg !18
#dbg_value(i1 %cmp, !19, !DIExpression(DW_OP_LLVM_convert, 1, DW_ATE_unsigned, DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_stack_value), !16)
br i1 %cmp, label %if.then, label %if.else, !dbg !20

if.then:
br label %return, !dbg !22

if.else:
br label %return, !dbg !23

return:
%retval.0 = phi i32 [ 12, %if.then ], [ 123, %if.else ], !dbg !24
ret i32 %retval.0, !dbg !25
}
```

After `instcombine`, the branch is canonicalized to `fcmp ueq` with swapped
successors:

```llvm
define dso_local i32 @f_one(float noundef %X, float noundef %Y) #0 !dbg !9 {
entry:
#dbg_value(float %X, !15, !DIExpression(), !16)
#dbg_value(float %Y, !17, !DIExpression(), !16)
%cmp = fcmp ueq float %X, %Y, !dbg !18
#dbg_value(i1 %cmp, !19, !DIExpression(DW_OP_LLVM_convert, 1, DW_ATE_unsigned, DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_stack_value), !16)
br i1 %cmp, label %if.else, label %if.then, !dbg !20

if.then:
br label %return, !dbg !22

if.else:
br label %return, !dbg !23

return:
%retval.0 = phi i32 [ 12, %if.then ], [ 123, %if.else ], !dbg !24
ret i32 %retval.0, !dbg !25
}
```

The optimized branch is semantically equivalent for the program because the branch
successors are swapped. But the `#dbg_value` for `C` still describes `%cmp` directly,
even though `%cmp` now means `ueq` instead of `one`.

For example, with `X = 1.0` and `Y = 2.0`:

```text
source C = islessgreater(1.0, 2.0) = true
src.ll %cmp = fcmp one 1.0, 2.0 = true
tgt.ll %cmp = fcmp ueq 1.0, 2.0 = false
```

So the debug value for `C` is inverted by the optimization.

## Expected Behavior

When replacing `fcmp one` with `fcmp ueq` and swapping the branch targets,
`instcombine` should also update the debug value for the original source predicate.

For this testcase, `C` should continue to describe the result of `fcmp one`. Possible
fixes could include preserving the original predicate value for the debug location or
adding an appropriate inverse operation in the `DIExpression` when the debug value is
rewritten to use the new `fcmp ueq` result.

## Debugger Note

This report is about the middle-end debug information visible in `tgt.ll`.

With the exact clang-24 pipeline above on my machine, the final binaries built from
both `src.ll` and `tgt.ll` do not contain debug information for local variable `C`.
I confirmed this by inspecting the debug information with `objdump`. Therefore LLDB
cannot show the inverted value directly.

I tried adjusting the source testcase to keep `C` visible in the final binary, but I
could not find a variant that both keeps `C` printable in LLDB and preserves the same
`fcmp one` to `fcmp ueq` InstCombine transformation. Therefore I am filing this as an
IR-level debug-info issue.

## Environment

```text
clang version 24.0.0git
llvm-project revision: b443896c13aded8b40d7bae4a5a9adbc96fd0d31

LLVM version 24.0.0git
```

## Notes

My guess is that the branch canonicalization updates the branch successors but treats
the `#dbg_value` as if it can keep referring to the new compare result unchanged. That
is not valid for `fcmp one -> fcmp ueq`, because the new predicate is the logical
inverse of the source-level value assigned to `C`.

Contributor guide

Open the contributing guide

Research direction

Run the listed clang/opt pipeline on case.c and compare src.ll with tgt.ll, focusing on the InstCombine fcmp one canonicalization. The payload names no implementation file or regression test, so locate the branch canonicalization entry point from the InstCombine sources. Done means the optimized IR preserves the original debug value for C while retaining equivalent branch behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.