llvm / llvm/llvm-project

[Hexagon] sfmin/sfmax select-lowering miscompile

Open
#210,132 2 comments 0 reactions 1 assignee Claimed by @iajbar View on GitHub
backend:Hexagon miscompilation
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

`clang -O2` for Hexagon lowers the IEEE-strict "pseudo-minimum" pattern

```c
float pmin(float a, float b) { return (b < a) ? b : a; }
```

to `sfmin(r1,r0)`, even though the LLVM IR is a plain `fcmp olt` + `select` with **no fast-math flags**. The C expression is fully defined for NaN (`b < a` is false when either operand is NaN, so the result must be `a`, i.e. `pmin(NaN, x) == NaN`), but `sfmin`/`sfmax` implement minNum-style NaN handling and return the non-NaN operand. The transform is only legal under `nnan`.

WASM spec suites `simd_f32x4_pmin_pmax`, `simd_f64x2_pmin_pmax` (and NaN-lane cases in `simd_lane` / `simd_splat`) fail on Hexagon: WAMR's fast interpreter executes `f32x4.pmin` etc. through SIMDe, whose portable implementation is exactly the pattern above. the Wasm spec mandates the `(b < a) ? b : a` semantics, so the miscompiled `sfmin` returns the wrong lane whenever the first operand is NaN.

```c
/*
* Minimal reproducer: clang miscompiles the IEEE-strict float-min pattern
* on Hexagon by selecting sfmin/sfmax for it.
*
* `pmin` is the Wasm SIMD "pseudo-minimum" lane operation exactly as the
* spec defines it and exactly as SIMDe implements it (WAMR's fast
* interpreter uses SIMDe for v128 ops):
*
* pmin(a, b) = (b < a) ? b : a
*
* The C semantics are fully defined for NaN: `b < a` is false when either
* operand is NaN, so the result must be `a` — i.e. pmin(NaN, x) == NaN.
*
* At -O2/-O3 the Hexagon backend lowers the `fcmp olt` + `select` pair to
* `sfmin` even though the IR carries no fast-math flags. sfmin implements
* minNum-style NaN handling (returns the non-NaN operand), so
* pmin(NaN, x) comes back as x. The same happens for pmax and for the
* vectorized form (four independent sfmin after unrolling), which is how
* WAMR's f32x4.pmin/pmax and f64x2.pmin/pmax spec tests fail on Hexagon.
*
* `pmin_ref` computes the same expression with the comparison made opaque
* so the pattern cannot be matched; it is the ground truth.
*
* The scalar direction that *appears* to work (pmin(a=NaN passed via r0)
* in some operand orders) is luck: sfmin's NaN behavior is not the
* select's, and which operand survives depends on register assignment.
*/
#include
#include
#include

__attribute__((noinline)) static float
pmin(float a, float b)
{
return (b < a) ? b : a;
}

__attribute__((noinline)) static float
pmax(float a, float b)
{
return (a < b) ? b : a;
}

__attribute__((noinline)) static float
pmin_ref(float a, float b)
{
volatile int lt = (b < a);
return lt ? b : a;
}

__attribute__((noinline)) static float
pmax_ref(float a, float b)
{
volatile int lt = (a < b);
return lt ? b : a;
}

static uint32_t
bits(float f)
{
uint32_t u;
memcpy(&u, &f, 4);
return u;
}

int
main(void)
{
volatile uint32_t nan_bits = 0x7fc00000; /* quiet NaN */
volatile uint32_t one_bits = 0x3f800000; /* 1.0f */
float nanf, one;
memcpy(&nanf, (void *)&nan_bits, 4);
memcpy(&one, (void *)&one_bits, 4);

struct {
const char *name;
float got, want;
} cases[] = {
{ "pmin(nan, 1.0)", pmin(nanf, one), pmin_ref(nanf, one) },
{ "pmin(1.0, nan)", pmin(one, nanf), pmin_ref(one, nanf) },
{ "pmax(nan, 1.0)", pmax(nanf, one), pmax_ref(nanf, one) },
{ "pmax(1.0, nan)", pmax(one, nanf), pmax_ref(one, nanf) },
};

int fails = 0;
for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
uint32_t got = bits(cases[i].got), want = bits(cases[i].want);
printf("%s got=0x%08x want=0x%08x %s\n", cases[i].name, got, want,
got == want ? "ok" : "<<< MISCOMPILE");
if (got != want)
fails++;
}
printf("\n%d failure(s)\n", fails);
return fails ? 1 : 0;
}

```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.