llvm / llvm/llvm-project

[DebugInfo][InstCombine] instcombine produces an incorrect debug value after simplifying a masked computation

Open
#212,427 2 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

After running `instcombine`, LLDB prints a different value for a source local variable in a small bit-manipulation test case.

The source value of `tmp2` is computed as:

```c
uint64_t tmp1 = (uint64_t)b;
uint64_t tmp2 = tmp1 << 32;
```

With the call `func(0, 1)`, the source-level value of `tmp2` should be `0x0000000100000000` / `4294967296`. Before `instcombine`, LLDB reports that value. After `instcombine`, LLDB reports `tmp2 = 0x0000000000000000`.

I am not sure about the exact root cause. `instcombine` preserves `#dbg_value` correctly in many other cases, but this case seems to mishandle the debug value while simplifying a computation whose high bits are later masked away.

## Reproducer

`case.c`:

```c
#include

uint64_t func(uint64_t a, uint32_t b) {
uint64_t tmp1 = (uint64_t)b;
uint64_t tmp2 = tmp1 << 32;
uint64_t tmp3 = tmp2 + a;
uint64_t tmp4 = tmp3 & 0x123;
return tmp4;
}

int main() {
func(0, 1);
return 0;
}
```

Build pipeline:

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

clang src.ll -o src.out
clang tgt.ll -o tgt.out
```

I will provide links to the full `src.ll` and `tgt.ll` in the issue rather than embedding the full IR [here](https://godbolt.org/z/TxjPGv1zM).

## Debugging Script

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

cd "$(dirname "$0")"

clang -g -O0 -Xclang -disable-O0-optnone -emit-llvm -S case.c -o case.ll
opt -passes="mem2reg" -S case.ll -o src.ll
opt -passes="instcombine" -S src.ll -o tgt.ll

clang src.ll -o src.out
clang tgt.ll -o tgt.out

for exe in src.out tgt.out; do
echo "== $exe =="
lldb --batch \
-o 'breakpoint set --file case.c --line 6' \
-o run \
-o 'frame variable --format hex tmp2' \
-- "./$exe"
echo
done
```

## Actual Behavior

Before `instcombine`, LLDB reports the expected value:

```text
== src.out ==
Breakpoint 1: where = src.out`func + 10 at case.c:6:24

frame #0: ... src.out`func(a=0, b=1) at case.c:6:24

(lldb) frame variable --format hex tmp2
(uint64_t) tmp2 = 0x0000000100000000
```

After `instcombine`, LLDB reports `0`:

```text
== tgt.out ==
Breakpoint 1: where = tgt.out`func + 7 at case.c:7:24

frame #0: ... tgt.out`func(a=0, b=1) at case.c:7:24

(lldb) frame variable --format hex tmp2
(uint64_t) tmp2 = 0x0000000000000000
```

Note that the breakpoint requested by the script is `case.c:6`; after optimization, LLDB resolves it to the next available location in the optimized code.

## Expected Behavior

LLDB should not report the wrong concrete value for `tmp2`.

At the source point after `tmp2` is initialized, `tmp2` should either:

- be shown as `0x0000000100000000`, matching `(uint64_t)1 << 32`, or
- be reported as unavailable/optimized out if the value cannot be preserved.

It should not be shown as `0x0000000000000000`.

## Observed Directly in `.ll`

This problem is visible directly in the optimized IR.

Before `instcombine`, `src.ll` computes `tmp2` from `b` and the debug value for `tmp2` follows the result of the shift:

```llvm
define dso_local i64 @func(i64 noundef %0, i32 noundef %1) #0 !dbg !15 {
#dbg_value(i64 %0, !22, !DIExpression(), !23)
#dbg_value(i32 %1, !24, !DIExpression(), !23)
%3 = zext i32 %1 to i64, !dbg !25
#dbg_value(i64 %3, !26, !DIExpression(), !23)
%4 = shl i64 %3, 32, !dbg !27
#dbg_value(i64 %4, !28, !DIExpression(), !23)
%5 = add i64 %4, %0, !dbg !29
#dbg_value(i64 %5, !30, !DIExpression(), !23)
%6 = and i64 %5, 291, !dbg !31
#dbg_value(i64 %6, !32, !DIExpression(), !23)
ret i64 %6, !dbg !33
}
...
!28 = !DILocalVariable(name: "tmp2", scope: !15, file: !1, line: 5, type: !3)
```

After `instcombine`, `tgt.ll` simplifies the program computation to the final masked result. That optimization is fine for the program value, because the high 32 bits from `tmp2` are not demanded by `and 0x123`. However, the debug value for `tmp2` is also rewritten to a constant zero shift:

```llvm
define dso_local i64 @func(i64 noundef %0, i32 noundef %1) #0 !dbg !15 {
#dbg_value(i64 %0, !22, !DIExpression(), !23)
#dbg_value(i32 %1, !24, !DIExpression(), !23)
#dbg_value(i32 undef, !25, !DIExpression(DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_LLVM_convert, 64, DW_ATE_unsigned, DW_OP_stack_value), !23)
#dbg_value(i64 0, !26, !DIExpression(DW_OP_constu, 32, DW_OP_shl, DW_OP_stack_value), !23)
#dbg_value(!DIArgList(i64 0, i64 %0), !27, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_plus, DW_OP_stack_value), !23)
%3 = and i64 %0, 291, !dbg !28
#dbg_value(i64 %3, !29, !DIExpression(), !23)
ret i64 %3, !dbg !30
}
...
!26 = !DILocalVariable(name: "tmp2", scope: !15, file: !1, line: 5, type: !3)
```

The resulting debug expression describes `0 << 32`, which is `0`, rather than `(uint64_t)b << 32`.

The final `and 0x123` makes the high 32-bit contribution irrelevant to the optimized program result, so replacing that contribution with zero is valid for the program value. However, that replacement is not a correct source-level debug value for `tmp2` itself.

## Environment

```text
clang version 23.0.0git
llvm-project revision: 58203500b5bd1628401c1a103eb264b7d78ea3bb

LLVM version 23.0.0git
LLDB version 23.0.0git
```

## Notes

I am not sure which part of `instcombine` is responsible for this. My guess is that the issue is related to simplifying the high-bit part of the expression because those bits are not demanded by the later `and 0x123`. That is fine for the optimized program result, but the same replacement should not be reused as the source variable value for `tmp2`.

Contributor guide

Open the contributing guide

Research direction

Start with case.c and run the provided debugging script to generate src.ll and tgt.ll through mem2reg and instcombine. Compare the #dbg_value entries for tmp2 around the simplified shift and inspect the instcombine pass entry point. Done means LLDB reports 0x0000000100000000 for tmp2, or marks it unavailable rather than reporting zero.

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
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.