`__builtin_bit_cast` of an ext_vector element yields lane 0 at run time, disagreeing with constant evaluation
- 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
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