https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127960

            Bug ID: 127960
           Summary: RISC-V soft-HF `_Float16` argument not NaN-boxed
           Product: gcc
           Version: 15.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: target
          Assignee: unassigned at gcc dot gnu.org
          Reporter: tim.besard at gmail dot com
  Target Milestone: ---

Created attachment 65879
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65879&action=edit
testcase

Without Zfh/Zfhmin, a _Float16 argument or return value lives in an FPR and
has to be NaN-boxed (bits 63:16 all ones), since a callee built with Zfh
treats any other bit pattern as the canonical NaN.  When the _Float16 value
comes from a bit-cast of an integer (memcpy, union, VIEW_CONVERT_EXPR), GCC
moves the integer to the FPR with a bare fmv.s.x.  That only fills bits 63:32
with ones; bits 31:16 are zero.

Testcase (no headers):

__attribute__((noipa, target ("arch=+zfh")))
void check (_Float16 x)
{
  if ((float) x != 1.5f)
    __builtin_abort ();
}

__attribute__((noipa))
void pass_bits (void (*cb) (_Float16), unsigned short u)
{
  _Float16 x;
  __builtin_memcpy (&x, &u, 2);
  cb (x);
}

static unsigned short buf;

__attribute__((noipa))
void pass_local (void (*cb) (_Float16))
{
  _Float16 x;
  __builtin_memcpy (&x, &buf, 2);
  cb (x);
}

int
main (void)
{
  pass_bits (check, 0x3e00);    /* 1.5 */
  buf = 0x3e00;
  pass_local (check);
  return 0;
}

$ riscv64-linux-gnu-gcc -O2 -march=rv64gc -mabi=lp64d -Wall -Wextra -fPIC
-static pr-f16-nanbox.c
$ qemu-riscv64 -cpu rv64,zfh=true ./a.out
qemu: uncaught target signal 6 (Aborted) - core dumped

There are no compiler diagnostics.  The test also aborts with -fno-PIC
(because of pass_bits) and at -O1 and -Os.  It passes at -O0, and it passes
when the whole file is built with -march=rv64gc_zfh.

GCC 15.2.0, -O2 -march=rv64gc -mabi=lp64d -fPIC:

pass_bits:
        fmv.s.x fa0,a1
        jr      a0
pass_local:
        lhu     a5,.LANCHOR0
        fmv.s.x fa0,a5
        jr      a0

Trunk (17.0.0 20261016, on Compiler Explorer, configured with
--enable-default-pie) produces the same code for pass_local, and for
pass_bits it now goes through the stack but still ends in a bare fmv.s.x:

pass_bits:
        addi    sp,sp,-16
        sh      a1,14(sp)
        lhu     a5,14(sp)
        addi    sp,sp,16
        fmv.s.x fa0,a5
        jr      a0

The expected sequence is what GCC already emits for a plain _Float16 load,
or for pass_local with -fno-PIC:

        lhu     a5,%lo(buf)(a5)
        li      a4,-65536
        or      a5,a4,a5
        fmv.w.x fa0,a5          # *movhf_softfloat_boxing

At expand time the bit-cast becomes

  (set (reg:HF fa0) (subreg:HF (reg:HI 138) 0))

The NaN-boxing code in riscv_legitimize_move (from r14-6916) only fires for
REG_P (src).  This source is a SUBREG, so the move is kept as is and matches
the "f <- r" alternative of *movhf_softfloat, which prints fmv.s.x.  With
-fno-PIC pass_local works by accident: combine folds the zero_extend load
into (set (reg:HF fa0) (mem:HF (lo_sum ...))), and LRA reloads that through
gen_movhf, which does box.  With -fPIC/-fPIE, *local_pic_loadhi is formed
first, so the subreg move survives until final.  -dp output for the
-fPIC case:

        lhu     a5,.LANCHOR0    # 8     [c=32 l=8]  *local_pic_loadhi
        fmv.s.x fa0,a5  # 9     [c=4 l=4]  *movhf_softfloat/4

The same happens for __bf16 (*movbf_softfloat/4 emits fmv.s.x).  Handling a
SUBREG of a non-FP register in that riscv_legitimize_move check would cover
the expand-time case.  The GPR->FPR alternative of *movhf_softfloat and
*movbf_softfloat can still print an unboxed fmv.s.x for anything created
after expand, though, so it should either be removed or box the value
itself.

GCC 13.x has no soft-HF boxing at all, so it miscompiles this and also
plain _Float16 loads.  12.x rejects _Float16 on RISC-V.  Known to fail:
every release from 13.1 to 16.1, and trunk 20261016.  I checked all of them
as asm on Compiler Explorer.  14.2 and 15.2 were also run under qemu-user
11.1 with Zfh enabled, which checks NaN-boxing on half-precision operations.

This was found through Julia's ccall ABI tests, which read NaN16 in a Zfh
callback on a SpacemiT X60 (JuliaLang/julia#63880). The diagnosis and report
was assisted by AI (Claude Code, Opus 5.5)

$ riscv64-linux-gnu-gcc -v
Using built-in specs.
COLLECT_GCC=riscv64-linux-gnu-gcc
COLLECT_LTO_WRAPPER=.../libexec/gcc/riscv64-linux-gnu/15.2.0/lto-wrapper
Target: riscv64-linux-gnu
Configured with: /workspace/srcdir/gcc-15.2.0/configure
--prefix=/workspace/destdir --build=x86_64-linux-musl --host=x86_64-linux-gnu
--target=riscv64-linux-gnu
--with-sysroot=/workspace/destdir/riscv64-linux-gnu/sys-root
--enable-languages=c,c++,fortran --enable-shared --enable-threads=posix
--enable-version-specific-runtime-libs --disable-multilib --disable-bootstrap
--disable-werror --disable-libcc1
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 15.2.0 (GCC)

Reply via email to