[DebugInfo][FlattenCFG] Merging equivalent blocks keeps debug locations from the wrong source branch
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
# [DebugInfo][FlattenCFG] Merging equivalent blocks keeps debug locations from the wrong source branch
## Description
`flatten-cfg` merges two equivalent `if` bodies into one block. The optimized program behavior is preserved, but the merged block keeps the debug locations from the second source branch.
In this testcase, `main` calls `func(0, 1)`. Therefore the first condition `a == 0` is true, while the second condition `b == 0` is false. Before `flatten-cfg`, LLDB steps into the body of the first `if (a == 0)`. After `flatten-cfg`, LLDB steps into the body of the second `if (b == 0)`, even though `b == 0` is false.
## Reproducer
`case.c`:
```c
int g = 100;
__attribute__((const, noinline)) int bar(void) { return 0; }
__attribute__((pure, noinline)) int read(const int *p) { return 0; }
__attribute__((noinline))int func(int a, int b) {
int p = 42;
if (a == 0) {
p = 100; // from if (a == 0)
(void)bar();
}
int val = g;
if (b == 0) {
p = 100; // from if (b == 0)
(void)bar();
}
return read(&p);
}
int main(void) { return func(0, 1); }
```
Build pipeline:
```sh
clang -g -O0 -Xclang -disable-O0-optnone -S -emit-llvm case.c -o case.ll
opt -passes=mem2reg -S case.ll -o src.ll
opt -passes=flatten-cfg -S src.ll -o tgt.ll
clang src.ll -o src.out
clang tgt.ll -o tgt.out
```
Debugging script:
```sh
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
./reproduce.sh
for exe in src.out tgt.out; do
echo "== $exe =="
lldb --batch \
-o 'breakpoint set --name func' \
-o run \
-o 'thread step-over' \
-o 'thread step-over' \
-o 'thread step-over' \
-o 'thread step-over' \
-- "./$exe"
echo
done
```
## Observed Behavior
Before `flatten-cfg`, LLDB steps through the first branch body:
```text
== src.out ==
func(a=0, b=1) at case.c:14:9
14 if (a == 0) {
func(a=0, b=1) at case.c:15:7
15 p = 100; // from if (a == 0)
func(a=0, b=1) at case.c:21:9
21 if (b == 0) {
```
After `flatten-cfg`, LLDB steps into the second branch body even though `b == 0` is false:
```text
== tgt.out ==
func(a=0, b=1) at case.c:14:9
14 if (a == 0) {
func(a=0, b=1) at case.c:21:9
21 if (b == 0) {
func(a=0, b=1) at case.c:22:7
22 p = 100; // from if (b == 0)
```
## Observed Directly in `.ll`
I will attach links to the full `src.ll` and `tgt.ll` files: [Full IR](https://godbolt.org/z/od3jMWsYh). The relevant parts are shown below.
Before `flatten-cfg`, the two source blocks have separate debug locations:
```llvm
%4 = icmp eq i32 %0, 0, !dbg !34
br i1 %4, label %5, label %7, !dbg !34
5:
store i32 100, ptr %3, align 4, !dbg !36
%6 = call i32 @bar() #3, !dbg !38
br label %7, !dbg !39
7:
%8 = load i32, ptr @g, align 4, !dbg !40
%9 = icmp eq i32 %1, 0, !dbg !42
br i1 %9, label %10, label %12, !dbg !42
10:
store i32 100, ptr %3, align 4, !dbg !44
%11 = call i32 @bar() #3, !dbg !46
br label %12, !dbg !47
```
The first block's `store` is line 15, and the second block's `store` is line 22:
```llvm
!36 = !DILocation(line: 15, column: 7, scope: !37)
!44 = !DILocation(line: 22, column: 7, scope: !45)
```
After `flatten-cfg`, the bodies are merged, and the merged block uses the second branch's debug locations:
```llvm
%4 = icmp eq i32 %0, 0, !dbg !34
%5 = load i32, ptr @g, align 4, !dbg !36
%6 = icmp eq i32 %1, 0, !dbg !38
%7 = or i1 %4, %6, !dbg !38
br i1 %7, label %8, label %10, !dbg !38
8:
store i32 100, ptr %3, align 4, !dbg !40
%9 = call i32 @bar() #3, !dbg !42
br label %10, !dbg !43
```
```llvm
!40 = !DILocation(line: 22, column: 7, scope: !41)
!42 = !DILocation(line: 23, column: 11, scope: !41)
```
## Expected Behavior
When the merged block is executed because `a == 0` is true and `b == 0` is false, the debugger should not appear to enter the body of `if (b == 0)`.
If `flatten-cfg` cannot preserve branch-specific source locations after merging the two blocks, the merged instructions should use a less misleading location rather than unconditionally using the second branch body's locations.
## Environment
```text
clang version 24.0.0git
llvm-project revision: 3387214f70edc41cfd4cacb02a46290de11abcc8
LLVM version 24.0.0git
lldb version 24.0.0git
```
## Notes
My guess is that `flatten-cfg` treats the two blocks as equivalent for program semantics and then keeps one block's debug locations when forming the merged block. That is misleading when the merged block can be reached because of the other source condition.
Contributor guide
Research direction
Run the supplied case.c reproducer through the mem2reg and flatten-cfg commands, then compare the debug locations in src.ll and tgt.ll. Start by examining the flatten-cfg pass behavior around equivalent block merging; done means the merged block no longer unconditionally uses the second branch body's locations when reached through the first condition.
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
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100