llvm / llvm/llvm-project

`__builtin_bit_cast` of an ext_vector element yields lane 0 at run time, disagreeing with constant evaluation

Open
#219,531 8 comments 2 reactions 0 assignees View on GitHub
clang:frontend good first issue
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

### Summary

`__builtin_bit_cast(uint32_t, v.y)`, where `v` has an `ext_vector_type`, produces the bits of
**lane 0** rather than lane 1. Every element accessor is affected — `.x` happens to be correct only
because it *is* lane 0 — and the subscript form `v[1]` is wrong the same way.

The constant evaluator handles the identical expression correctly, so the same source has two
different answers in one translation unit: the `static_assert`s in the repro all pass while the
`printf`s all print `3f800000`.

There is no diagnostic, at any warning level.

### Reproducer

https://godbolt.org/z/63cWajavT

```c++
#include
#include

using float4 = float __attribute__((ext_vector_type(4)));

// Constant evaluation is correct: these all pass.
constexpr float4 kV = { 1.0f, 2.0f, 3.0f, 4.0f };
static_assert(__builtin_bit_cast(uint32_t, kV.x) == 0x3f800000u);
static_assert(__builtin_bit_cast(uint32_t, kV.y) == 0x40000000u);
static_assert(__builtin_bit_cast(uint32_t, kV.z) == 0x40400000u);
static_assert(__builtin_bit_cast(uint32_t, kV.w) == 0x40800000u);

int main() {
float4 v = { 1.0f, 2.0f, 3.0f, 4.0f };

// Codegen is not: every one of these prints 3f800000, which is lane 0.
printf("v.x %08x (expected 3f800000)\n", __builtin_bit_cast(uint32_t, v.x));
printf("v.y %08x (expected 40000000)\n", __builtin_bit_cast(uint32_t, v.y));
printf("v.z %08x (expected 40400000)\n", __builtin_bit_cast(uint32_t, v.z));
printf("v.w %08x (expected 40800000)\n", __builtin_bit_cast(uint32_t, v.w));

// The subscript form is wrong the same way.
printf("v[1] %08x (expected 40000000)\n", __builtin_bit_cast(uint32_t, v[1]));

// Materialising the element into a float first is correct.
const float y = v.y;
printf("copy %08x (expected 40000000)\n", __builtin_bit_cast(uint32_t, y));
printf("cast %08x (expected 40000000)\n", __builtin_bit_cast(uint32_t, float(v.y)));

return 0;
}
```

### Expected output

```
v.x 3f800000 (expected 3f800000)
v.y 40000000 (expected 40000000)
v.z 40400000 (expected 40400000)
v.w 40800000 (expected 40800000)
v[1] 40000000 (expected 40000000)
copy 40000000 (expected 40000000)
cast 40000000 (expected 40000000)
```

### Actual output

```
v.x 3f800000 (expected 3f800000)
v.y 3f800000 (expected 40000000)
v.z 3f800000 (expected 40400000)
v.w 3f800000 (expected 40800000)
v[1] 3f800000 (expected 40000000)
copy 40000000 (expected 40000000)
cast 40000000 (expected 40000000)
```

### Analysis

`v.y` is an `ExtVectorElementExpr` and `v[1]` an `ArraySubscriptExpr` over a vector; both are
lvalues designating a vector element. As the operand of `__builtin_bit_cast` each undergoes
lvalue-to-rvalue conversion and should yield a `float` prvalue holding that element's value, which
is what the constant evaluator does. Codegen appears to take the address of the whole vector and
load `sizeof(uint32_t)` bytes from its start, discarding the element index — which is consistent
with all four accessors and the subscript form returning lane 0.

The workaround is to materialise the element before the cast: binding it to a named `float`, or
writing `float(v.y)`, both give the right answer.

### Why this is worth prioritising

It is silent in a particularly unhelpful way. The natural use of this construct is an exact
comparison, and the failure mode is that the comparison starts *agreeing* with everything:

```c++
bool isBitIdentical(float3 a, float3 b) {
return __builtin_bit_cast(uint32_t, a.x) == __builtin_bit_cast(uint32_t, b.x)
&& __builtin_bit_cast(uint32_t, a.y) == __builtin_bit_cast(uint32_t, b.y)
&& __builtin_bit_cast(uint32_t, a.z) == __builtin_bit_cast(uint32_t, b.z);
}
```

This compares `a.x` against `b.x` three times and answers `true` for any pair differing only in y
or z. In our case it was the check backing a mesh watertightness test suite, and it had been
passing on that basis for months — nothing detected it until a test happened to translate geometry
along z.

### Environment

- clang version 23.1.0 (`ea7d852a70e8bdfaf601d6626a760f9771b2c4b4`)
- Target: `x86_64-pc-windows-msvc`
- Reproduces at `-O0` and `-O2`, `-std=c++20`

Contributor guide

Open the contributing guide

Research direction

Start with code generation for __builtin_bit_cast when its operand is an ExtVectorElementExpr or an ArraySubscriptExpr over a vector, comparing it with constant evaluation. Reproduce the supplied C++20 example at -O0 and -O2; done means .x/.y/.z/.w and v[1] produce their selected lane at run time, matching the static_assert results.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.