Skip to content

Instantly share code, notes, and snippets.

@shicks
Last active January 31, 2022 19:01
Show Gist options
  • Save shicks/7a97ec6b3f10212e60a89a7f6d2d097d to your computer and use it in GitHub Desktop.
Save shicks/7a97ec6b3f10212e60a89a7f6d2d097d to your computer and use it in GitHub Desktop.
Universal polyfill for Math.fround, does not depend on Float32Array.
// NOTE: This implementation is incorrect (it doesn't break ties to even). See comments below for discussion.
Math.fround = Math.fround || function(arg) {
arg = Number(arg);
// Return early for ±0 and NaN.
if (!arg) return arg;
var sign = arg < 0 ? -1 : 1;
if (sign < 0) arg = -arg;
// Compute the exponent (8 bits, signed).
var exp = Math.floor(Math.log(arg) / Math.LN2);
var powexp = Math.pow(2, Math.max(-126, Math.min(exp, 127)));
// Handle subnormals: leading digit is zero if exponent bits are all zero.
var leading = exp < -127 ? 0 : 1;
// Compute 23 bits of mantissa, inverted to round toward zero.
var mantissa = Math.round((leading - arg / powexp) * 0x800000);
if (exp > 0 && mantissa <= -0x800000) return sign * Infinity;
return sign * powexp * (leading - mantissa / 0x800000);
};
it('should handle terminating numbers', function() { [5/32762]
// These cases all have terminating binary representations (i.e. integers
// or rationals with powers of two in the denominator).
assertPositiveZero(Math.fround(0));
assertNegativeZero(Math.fround(-0));
assertEquals(1, Math.fround(1));
assertEquals(-1, Math.fround(-1));
assertEquals(0.5, Math.fround(0.5));
assertEquals(0.25, Math.fround(0.25));
assertEquals(-0.75, Math.fround(-0.75));
assertEquals(3, Math.fround(3));
assertEquals(-20.375, Math.fround(-20.375));
assertEquals(101.3125, Math.fround(101.3125));
assertEquals(1 << 22, Math.fround(1 << 22));
});
it('should handle large numbers', function() {
assertEquals(1 << 30, Math.fround(1 << 30));
assertEquals(2 ** 127, Math.fround(2 ** 127));
assertEquals(-(2 ** 127), Math.fround(-(2 ** 127)));
assertEquals(1.875 * (2 ** 127), Math.fround(1.875 * (2 ** 127)));
assertEquals(-1.9375 * (2 ** 127), Math.fround(-1.9375 * (2 ** 127)));
assertEquals('a', Infinity, Math.fround(2 ** 128));
assertEquals(-Infinity, Math.fround(-(2 ** 128)));
const maxFloat = 3.4028234663852886e38;
assertEquals(maxFloat, Math.fround(3.4028235e38));
assertEquals(Infinity, Math.fround(3.4028236e38));
assertEquals('b', Infinity, Math.fround(Infinity));
assertEquals(-maxFloat, Math.fround(-3.4028235e38));
assertEquals(-Infinity, Math.fround(-3.4028236e38));
assertEquals(-Infinity, Math.fround(-Infinity));
});
it('should handle small numbers', function() {
// Smallest normal float32
assertEquals(1.015625 * 2 ** -126, Math.fround(1.015625 * 2 ** -126));
assertEquals(-1.015625 * 2 ** -126, Math.fround(-1.015625 * 2 ** -126));
// Subnormal numbers
assertEquals(1.015625 * 2 ** -127, Math.fround(1.015625 * 2 ** -127));
assertEquals(1.875 * 2 ** -128, Math.fround(1.875 * 2 ** -128));
// Numbers exactly between two floats round toward zero
const minFloat = 2 ** -149;
assertEquals(12 * minFloat, Math.fround(12.5 * minFloat));
assertEquals(13 * minFloat, Math.fround(12.5 * (1 + EPSILON) * minFloat));
assertEquals(-12 * minFloat, Math.fround(-12.5 * minFloat));
assertEquals(-13 * minFloat, Math.fround(-12.5 * (1 + EPSILON) * minFloat));
// Smallest non-zero float32
assertEquals(minFloat, Math.fround(minFloat));
assertEquals(-minFloat, Math.fround(-minFloat));
assertEquals(minFloat, Math.fround((1 + EPSILON) / 2 * minFloat));
assertEquals(-minFloat, Math.fround(-(1 + EPSILON) / 2 * minFloat));
assertPositiveZero(Math.fround(minFloat / 2));
assertNegativeZero(Math.fround(-minFloat / 2));
// Edge cases around mantissa === -0x800000
assertEquals(2 ** -125, Math.fround(2 ** -125));
assertEquals(2 ** -124, Math.fround(2 ** -124));
});
it('should handle non-numbers', function() {
assertEquals(1, Math.fround(noCheck('1')));
assertExactlyNaN(Math.fround(noCheck([1, 2])));
assertExactlyNaN(Math.fround(noCheck('a')));
assertExactlyNaN(Math.fround(NaN));
});
@shicks
Copy link
Author

shicks commented Nov 10, 2019

I don't fully understand the difference with the subnormals, nor why it should or shouldn't affect the results - I've long since paged this arcana out of my brain.

Nice find with the infinities. 2 ** -124 is also problematic. It looks like line 14 needs to also check exp > 0. I added a new revision and included your test case.

@sjrd
Copy link

sjrd commented Jan 31, 2022

This polyfill is the first one that shows up on a Google search, and seems to be the one featured on MDN. Yet, it is incorrect, as it does not break ties to-even when rounding. The following test cases fail:

function assertExactEquals(expected, actual) {
  if (!Object.is(expected, actual)) {
    throw new Error(`expected ${expected} but got ${actual}`);
  }
}

it('some tests that fail', function() {
  function test(expected, value) {
    assertExactEquals(expected, Math.fround(value))
  }
  
  test(3.4000265159298767e-40, 3.400019509437555e-40) // even is upwards
  test(1.1754943508222875e-38, 1.1754942807573643e-38) // even is upwards (it's min-normal)
  test(3.40000057220459, 3.4000004529953003) // even is upwards
  test(Infinity, 3.4028235677973366e38)
});

Here is a correct implementation that rounds-to-nearest and breaks-ties-to-even. It is the implementation used by Scala.js:

Math.fround = Math.fround || function(v) {
  v = Number(v);
  if (!v) return v;
  var isNegative = v < 0;
  var av = isNegative ? -v : v;
  var absResult;
  if (av >= 3.4028235677973366e38) { // also handles the case av === Infinity
    absResult = Infinity;
  } else if (av >= 1.1754943508222875e-38) { // threshold for normal representations
    var e = Math.floor(Math.log(av) / 0.6931471805599453); // Math.LN2
    var twoPowE = Math.pow(2, e);
    var significand = av / twoPowE;
    /* Because of loss of precision in its computation (Math.log is not
     * guaranteed to return the best approximation), e might be 1 up or down,
     * which causes twoPowE and significand to be a factor 2 up or down.
     * We now adjust that so that significand is really in the range [1.0, 2.0).
     */
    if (significand < 1) {
      twoPowE = twoPowE / 2;
      significand = significand * 2;
    } else if (significand >= 2) {
      twoPowE = twoPowE * 2;
      significand = significand / 2;
    };
    // Round the significand to 23 bits of fractional part, and multiply back by twoPowE
    // Break ties to even
    // The constant is (min positive double value) / (1.0 / 2**23)
    absResult = ((significand * 4.144523e-317) / 4.144523e-317) * twoPowE;
  } else {
    // Round the value to a multiple of the smallest Float ULP
    // Break ties to even
    // The constant is (min positive double value) / (min positive float value)
    absResult = (av * 3.5257702653609953e-279) / 3.5257702653609953e-279;
  };
  return isNegative ? -absResult : absResult;
}

and here is the full tests of Scala.js, translated for the format above:

var FloatMin = 1.401298464324817e-45
var FloatMax = 3.4028234663852886e38

function assertExactEquals(expected, actual) {
  if (!Object.is(expected, actual))
    throw new Error(`expected ${expected} but got ${actual}`);
}

it('tests from Scala.js', function() {
  function test(expected, value) {
    assertExactEquals(expected, Math.fround(value))
  }
  
  // Values based on the limits of Doubles

  // Normal forms
  test(0.0, 2.2250738585072014e-308)  // smallest pos normal form
  test(Infinity, 1.7976931348623157e308)   // largest pos normal form
  test(1.879076684348498e23, 1.8790766677624812e23)    // an arbitrary pos normal form
  test(-0.0, -2.2250738585072014e-308) // smallest neg normal form
  test(-Infinity, -1.7976931348623157e308)  // largest neg normal form
  test(-1.879076684348498e23, -1.8790766677624812e23)   // an arbitrary neg normal form

  // Some corner cases of doubleToLongBits
  test(9.007199254740992e15, 9007199254740991.0)
  test(8.988465623066525e30, 8.988465674311579e+30)
  test(5.915260786662124e-27, 5.915260930833876e-27)
  test(4.450147769049213e-30, 4.450147717014403e-30)

  // Subnormal forms (they all underflow)
  test(0.0, 4.9e-324)                  // smallest pos subnormal form
  test(0.0, 2.225073858507201e-308)    // largest pos subnormal form
  test(0.0, 1.719471609939382e-308)    // an arbitrary pos subnormal form
  test(-0.0, -4.9e-324)                // smallest neg subnormal form
  test(-0.0, -2.225073858507201e-308)  // largest neg subnormal form
  test(-0.0, -1.719471609939382e-308)  // an arbitrary neg subnormal form

  // Values based on the limits of Floats

  // Around Float.MinPositiveValue.toDouble / 2.0
  test(0.0, 7.006492321624084e-46) // just below
  test(0.0, 7.006492321624085e-46) // Float.MinPositiveValue.toDouble / 2.0
  test(FloatMin, 7.006492321624087e-46) // just above

  // Around FloatMin
  test(FloatMin, 1.40129e-45)
  test(FloatMin, 1.401298464324812e-45)
  test(FloatMin, 1.401298464324832e-45)
  test(FloatMin, 1.40131e-45)

  // Around 3.4e-40f, which is a subnormal value
  test(3.39999848996059e-40, 3.39999848996058e-40)
  test(3.39999848996059e-40, 3.39999848996059e-40)
  test(3.39999848996059e-40, 3.3999984899606e-40)

  // Around 3.4000054964529118e-40, which is the midpoint between 3.4e-40f and 3.40001e-40f
  test(3.39999848996059e-40, 3.4000054964529114e-40)
  test(3.39999848996059e-40, 3.4000054964529118e-40) // even is downwards
  test(3.4000125029452334e-40, 3.400005496452912e-40)

  // Around 3.400019509437555e-40, which is the midpoint between 3.40001e-40f and 3.40003e-40f
  test(3.4000125029452334e-40, 3.4000195094375546e-40)
  test(3.4000265159298767e-40, 3.400019509437555e-40) // even is upwards
  test(3.4000265159298767e-40, 3.4000195094375554e-40)

  // Around 1.1754942807573643e-38, which is the midpoint between max-subnormal and min-normal
  test(1.1754942106924411e-38, 1.1754942807573642e-38)
  test(1.1754943508222875e-38, 1.1754942807573643e-38) // even is upwards (it's min-normal)
  test(1.1754943508222875e-38, 1.1754942807573644e-38)

  // Around 2.3509886e-38f, which is the max value with ulp == MinPosValue
  test(2.3509885615147286e-38, 2.3509885615147283e-38)
  test(2.3509885615147286e-38, 2.3509885615147286e-38)
  test(2.3509885615147286e-38, 2.3509885615147283e-38)

  // Around 2.3509887e-38f, which is the min value with ulp != MinPosValue
  test(2.350988701644575e-38, 2.3509887016445748e-38)
  test(2.350988701644575e-38, 2.350988701644575e-38)
  test(2.350988701644575e-38, 2.3509887016445755e-38)

  // Around 3.400000214576721, which is the midpoint between 3.4f and 3.4000003f (normals)
  test(3.4000000953674316, 3.4000002145767207)
  test(3.4000000953674316, 3.400000214576721) // even is downwards
  test(3.4000003337860107, 3.4000002145767216)

  // Around 3.4000004529953003, which is the midpoint between 3.4000003f and 3.4000006f (normals)
  test(3.4000003337860107, 3.4000004529953)
  test(3.40000057220459, 3.4000004529953003) // even is upwards
  test(3.40000057220459, 3.4000004529953007)

  // Around 3.4028235677973366e38, which is the midpoint between Float.MaxValue and Infinity
  test(FloatMax, 3.4028235677973362e38)
  test(Infinity, 3.4028235677973366e38)
  test(Infinity, 3.402823567797337e38)
});

it('tests from Scala.js no loss', function() {
  function test(value) {
    assertExactEquals(value, Math.fround(value))
  }

  // Specials
  test(+0.0)
  test(-0.0)
  test(Infinity)
  test(-Infinity)
  test(NaN)

  // Other corner cases

  test(FloatMin)
  test(-FloatMin)
  test(1.1754942106924411e-38) // max subnormal value
  test(-1.1754942106924411e-38)
  test(1.1754943508222875e-38) // min normal value
  test(-1.1754943508222875e-38)
  test(2.3509885615147286e-38) // max value with ulp == MinPosValue
  test(-2.3509885615147286e-38)
  test(2.350988701644575e-38) // min value with ulp != MinPosValue
  test(-2.350988701644575e-38)
  test(FloatMax)
  test(-FloatMax)

  // Some normal values

  test(3.4000000953674316)
  test(-3.4000000953674316)
  test(3.423000153928881e36)
  test(-3.423000153928881e36)

  // Some subnormal values

  test(3.39999848996059e-40)
  test(-3.39999848996059e-40)
  test(3.4191682529525537e-43)
  test(-3.4191682529525537e-43)
});

/cc @shicks

EDIT(sdh): avoided braceless wrapped ifs (see #gotofail vulnerability)

@shicks
Copy link
Author

shicks commented Jan 31, 2022

Thanks for finding that - I added a comment to the top of the gist to hopefully direct anyone to your comment.

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