Skip to content

[Hexagon] sfmin/sfmax select-lowering miscompile #210132

Description

@androm3da

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

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.

/*
 * 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 <stdint.h>
#include <stdio.h>
#include <string.h>

__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;
}

Metadata

Metadata

Assignees

Type

No type

Projects

Status
Needs Fix

Relationships

None yet

Development

No branches or pull requests

Issue actions