llvm / llvm/llvm-project

[DebugInfo][FlattenCFG] Merging equivalent blocks keeps debug locations from the wrong source branch

Open
#217,606 1 comment 0 reactions 0 assignees View on GitHub
debuginfo llvm:transforms
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.