Skip to content

NaN inputs whose payload lies below the target precision are rounded to Infinity #13

Open

Description

@sw23

This is bug 2 of 3 identified by comparing floating point number conversion results across a few open-source implementations. It appears that NAN inputs are converting to INF in some cases. Feel free to cancel if this is expected/intended.

Below is the full AI-generated summary:

A NaN input must give a NaN. CPFloat rounds a NaN's bit pattern as if it were a finite value, so when every set payload bit lies below the target precision, the payload is rounded away and the result is the Infinity pattern:

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cpfloat_binary64.h"

static double round_to(const char *format, cpfloat_rounding_t mode, double x) {
  optstruct *o = init_optstruct();
  strcpy(o->format, format);
  cpfloat_populate_optstruct_from_format(o);
  o->round = mode;
  double y;
  cpfloat(&y, &x, 1, o);
  free_optstruct(o);
  return y;
}

int main(void) {
  uint64_t bits = 0x7FF0000000000001;  /* a signalling NaN */
  double snan;
  memcpy(&snan, &bits, sizeof snan);
  printf("%g\n", round_to("binary16", CPFLOAT_RND_NE, snan)); /* inf, want nan */
  printf("%g\n", round_to("binary16", CPFLOAT_RND_TZ, snan)); /* inf, want nan */
  printf("%g\n", round_to("E4M3", CPFLOAT_RND_NE, snan));     /* inf, want nan */
  printf("%g\n", round_to("binary16", CPFLOAT_RND_NE, NAN));  /* nan (correct) */
  return 0;
}

cc -std=c11 -Icpfloat/src t.c -lm && ./a.out at 3583976 prints inf inf inf nan.

It happens under RND_NE, RND_TN and RND_TZ. RND_TP keeps these NaNs only because its rounding increment happens to carry into the kept bits. The quiet NaN survives because its quiet bit is the top fraction bit, which every format keeps, except at precision 1 (a custom format), where even 0x7FF8000000000000 becomes Infinity. With a custom exponent range, some NaNs come back as ±0 instead.

Cause: the scalar rounding macros (e.g. RN_TIES_TO_EVEN_SCALAR_OTHER_EXP in src/cpfloat_template.h) check underflow and overflow with < / >= comparisons, which are false for a NaN, so the NaN falls through to the bit-masking step (INTOF(y) + ...) & leadmask, which clears its low payload bits. Passing NaNs through before rounding (e.g. if (isnan(*(y))) *(x) = *(y); else ...), or forcing a quiet NaN, fixes it. Even if signalling NaNs are out of scope, quieting one would be fine; turning it into Infinity is not.

Use the following links to see the expected conversion results:

Activity

  1. mogrob commented on Oct 13, 2026

    @mogrob

    Another good catch – thanks!

    For NaN conversion, your tool always sets the first payload bit of the output to 1. Is there a reason for this choice? When the target format is narrower, this is as good a choice as any other, but when the target format is wider, the payload could be entirely preserved.

  2. sw23 commented on Oct 14, 2026

    @sw23
    Author

    For NaN conversion, your tool always sets the first payload bit of the output to 1. Is there a reason for this choice? When the target format is narrower, this is as good a choice as any other, but when the target format is wider, the payload could be entirely preserved.

    Agreed, and no, there wasn't a strong reason for the MSB = 1 behavior. It seems like adding an "Advanced" mode NaN behavior option may be helpful in some cases, as there appear to be implementations that support both. I'll make a note to add that. Thanks for the feedback! Open to any other thoughts or suggestions as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions