llvm / llvm/llvm-project

[DebugInfo][InstCombine] Canonicalizing a shift-based truncation check omits an intermediate truncation from the debug expression

Open
#218,684 1 comment 0 reactions 0 assignees View on GitHub
debuginfo llvm:instcombine
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Description

InstCombine recognizes a comparison against a value produced by left-shifting, truncating to `int8_t`, and arithmetic-right-shifting, and replaces the comparison with an equivalent range check. The program behavior is preserved, but the salvaged `#dbg_value` for the source variable `restored` does not represent the intermediate truncation to eight bits.

For `x = 0x80`, the left shift produces `0x1000`, which is explicitly truncated through `uint8_t` before being stored in the `int8_t` variable `shifted`. Therefore, both `shifted` and `restored` have the source-level value `0x00`. After InstCombine, the debug expression for `restored` applies `shl` and `shra` directly to `x` without truncating the left-shift result first. LLDB and GDB consequently report `restored = 0x80` instead of `0x00`.

The optimized DWARF reflects the same problem. The DIE for `shifted` contains only the left shift, so the final `int8_t` type still causes the displayed value to be truncated to `0x00`. The DIE for `restored` performs the arithmetic right shift before any eight-bit truncation, producing `0x80`, which is then displayed as the signed `int8_t` bit pattern `0x80`.

## Reproducer

`case.c`:

```c
#include
#include

bool shift_truncation_check(int8_t x) {
int8_t shifted = (uint8_t)((uint8_t)x << 5);
int8_t restored = shifted >> 5;
return x == restored;
}

int main(void) { return !shift_truncation_check(0x80); }
```

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 -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
```

Here are the complete [src.ll and tgt.ll](https://godbolt.org/z/bPWGMM39c).

`lldb-commands.txt`:

```text
breakpoint set --file case.c --line 7
run
frame variable --format hex restored
quit
```

Run LLDB on the binaries before and after InstCombine:

```sh
lldb src.out -s lldb-commands.txt
lldb tgt.out -s lldb-commands.txt
```

## Observed Behavior

Before InstCombine, the IR computes and records the intermediate `i8` values explicitly:

```llvm
%conv1 = trunc i32 %shl to i8, !dbg !28
#dbg_value(i8 %conv1, !29, !DIExpression(), !25)
%conv2 = sext i8 %conv1 to i32, !dbg !30
%shr = ashr i32 %conv2, 5, !dbg !31
%conv3 = trunc i32 %shr to i8, !dbg !30
#dbg_value(i8 %conv3, !32, !DIExpression(), !25)
```

After InstCombine, the shift operations are removed from the executable code and represented only by salvaged debug expressions:

```llvm
#dbg_value(i8 %x, !26, !DIExpression(DW_OP_constu, 5, DW_OP_shl, DW_OP_stack_value), !25)
#dbg_value(i8 %x, !27, !DIExpression(DW_OP_constu, 5, DW_OP_shl, DW_OP_constu, 5, DW_OP_shra, DW_OP_stack_value), !25)
%0 = add i8 %x, 4, !dbg !28
%cmp = icmp ult i8 %0, 8, !dbg !28
```

Here, `!26` is `shifted` and `!27` is `restored`:

```llvm
!26 = !DILocalVariable(name: "shifted", scope: !15, file: !1, line: 5, type: !19)
!27 = !DILocalVariable(name: "restored", scope: !15, file: !1, line: 6, type: !19)
```

The incorrect expression for `restored` is already present in `tgt.ll` immediately after running `opt -passes=instcombine`. The backend lowers the same expression into DWARF, and both LLDB and GDB evaluate the resulting DIE consistently.

LLDB reports the source-level value before InstCombine:

```text
== src.out ==
(int8_t) restored = 0x00
```

After InstCombine, LLDB reports a different value:

```text
== tgt.out ==
(int8_t) restored = 0x80
```

GDB reports the same values as LLDB before and after InstCombine.

## Expected Behavior

At line 7, LLDB and GDB should report the same source-level value before and after InstCombine:

```text
(int8_t) restored = 0x00
```

## DWARF DIEs

The variable DIEs can be inspected with:

```sh
llvm-dwarfdump --debug-info --name=shifted tgt.out
llvm-dwarfdump --debug-info --name=restored tgt.out
```

The optimized DIEs contain reconstructed expressions based on `x`:

```text
0x0000003e: DW_TAG_variable
DW_AT_location (indexed (0x1) loclist = 0x0000001e:
[0x0000000000001137, 0x0000000000001139): DW_OP_breg0 RAX+0, DW_OP_constu 0xff, DW_OP_and, DW_OP_lit5, DW_OP_shl, DW_OP_stack_value)
DW_AT_name ("shifted")
DW_AT_decl_file ("case.c")
DW_AT_decl_line (5)
DW_AT_type (0x0000007c "int8_t")

0x00000047: DW_TAG_variable
DW_AT_location (indexed (0x2) loclist = 0x0000002c:
[0x0000000000001137, 0x0000000000001139): DW_OP_breg0 RAX+0, DW_OP_constu 0xff, DW_OP_and, DW_OP_lit5, DW_OP_shl, DW_OP_lit5, DW_OP_shra, DW_OP_stack_value)
DW_AT_name ("restored")
DW_AT_decl_file ("case.c")
DW_AT_decl_line (6)
DW_AT_type (0x0000007c "int8_t")
```

Under DWARF v5, the stack entries produced by `DW_OP_breg0`, `DW_OP_constu`, and `DW_OP_lit5` have the generic type, whose size is the target address size. On x86-64, the debugger therefore evaluates the arithmetic in both location expressions as 64-bit operations. Neither expression narrows the stack value to eight bits while it is being evaluated; truncation occurs only when the final result is interpreted using the DIE's `int8_t` type.

For `shifted`, the expression computes `0x80 << 5` as the 64-bit value `0x1000`, which is finally truncated to `0x00` when displayed as `int8_t`. For `restored`, the expression computes the same 64-bit left shift and then performs `DW_OP_shra` before truncation, producing `0x80`. Only then is the result interpreted as `int8_t`, so the debugger displays `0x80`.

The `restored` expression therefore does not represent the source-level assignment to `shifted`.

## Note

Based on my experience with LLVM optimizations, mismatches between an operand's width and the target address width are normally handled in the middle end. I have seen other passes add explicit conversions for this purpose, so I believe the optimization that creates these salvaged debug expressions should be responsible for preserving the required operand widths.

## Environment

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

LLVM version 24.0.0git
lldb version 24.0.0git
```

Contributor guide

Open the contributing guide

Research direction

Start with the case.c reproducer and run the listed opt -passes=instcombine pipeline, comparing the debug values in src.ll and tgt.ll. Trace the InstCombine handling that salvages the expressions for shifted and restored, then verify that the optimized expression preserves the intermediate eight-bit truncation and that LLDB reports restored as 0x00.

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.