Added HexFloat reading to HexFloat.
authorAndrew Woloszyn <awoloszyn@google.com>
Fri, 23 Oct 2015 17:26:02 +0000 (13:26 -0400)
committerDavid Neto <dneto@google.com>
Mon, 2 Nov 2015 18:52:28 +0000 (13:52 -0500)
This allows reading of hex-encoded floats.

include/util/hex_float.h
test/HexFloat.cpp

index 1583bb8..d03a0f9 100644 (file)
@@ -28,6 +28,7 @@
 #define _LIBSPIRV_UTIL_HEX_FLOAT_H_
 
 #include <cassert>
+#include <cctype>
 #include <cmath>
 #include <cstdint>
 #include <iomanip>
@@ -151,6 +152,19 @@ class HexFloat {
                 "The number of bits do not fit");
 };
 
+// Returns 4 bits represented by the hex character.
+inline uint8_t get_nibble_from_character(char character) {
+  const char* dec = "0123456789";
+  const char* lower = "abcdef";
+  const char* upper = "ABCDEF";
+  if (auto p = strchr(dec, character)) return p - dec;
+  if (auto p = strchr(lower, character)) return p - lower + 0xa;
+  if (auto p = strchr(upper, character)) return p - upper + 0xa;
+
+  assert(false && "This was called with a non-hex character");
+  return 0;
+}
+
 // Outputs the given HexFloat to the stream.
 template <typename T, typename Traits>
 std::ostream& operator<<(std::ostream& os, const HexFloat<T, Traits>& value) {
@@ -217,6 +231,227 @@ std::ostream& operator<<(std::ostream& os, const HexFloat<T, Traits>& value) {
   os << "p" << std::dec << (int_exponent >= 0 ? "+" : "") << int_exponent;
   return os;
 }
+
+template <typename T, typename Traits>
+inline std::istream& ParseNormalFloat(
+    std::istream& is, bool negate_value, HexFloat<T, Traits>& value) {
+  T val;
+  is >> val;
+  if (negate_value) {
+    val = -val;
+  }
+  value.set_value(val);
+  return is;
+}
+
+// Reads a HexFloat from the given stream.
+// If the float is not encoded as a hex-float then it will be parsed
+// as a regular float.
+// This may fail if your stream does not support at least one unget.
+// Nan values can be encoded with "0x1.<not zero>p+exponent_bias".
+// This would normally overflow a float and round to
+// infinity but this special pattern is the exact representation for a NaN,
+// and therefore is actually encoded as the correct NaN. To encode inf,
+// either 0x0p+exponent_bias can be spcified or any exponent greater than
+// exponent_bias.
+// Examples using IEEE 32-bit float encoding.
+//    0x1.0p+128 (+inf)
+//    -0x1.0p-128 (-inf)
+//
+//    0x1.1p+128 (+Nan)
+//    -0x1.1p+128 (-Nan)
+//
+//    0x1p+129 (+inf)
+//    -0x1p+129 (-inf)
+template <typename T, typename Traits>
+std::istream& operator>>(std::istream& is, HexFloat<T, Traits>& value) {
+  using HF = HexFloat<T, Traits>;
+  using uint_type = typename HF::uint_type;
+  using int_type = typename HF::int_type;
+
+  value.set_value(T(0));
+
+  if (is.flags() & std::ios::skipws) {
+    // If the user wants to skip whitespace , then we should obey that.
+    while (std::isspace(is.peek())) {
+      is.get();
+    }
+  }
+
+  char next_char = is.peek();
+  bool negate_value = false;
+
+  if (next_char != '-' && next_char != '0') {
+      return ParseNormalFloat(is, negate_value, value);
+  }
+
+  if (next_char == '-') {
+    negate_value = true;
+    is.get();
+    next_char = is.peek();
+  }
+
+  if (next_char == '0') {
+    is.get();  // We may have to unget this.
+    char maybe_hex_start = is.peek();
+    if (maybe_hex_start != 'x' && maybe_hex_start != 'X') {
+      is.unget();
+      return ParseNormalFloat(is, negate_value, value);
+    } else {
+      is.get();  // Throw away the 'x';
+    }
+  } else {
+    return ParseNormalFloat(is, negate_value, value);
+  }
+
+  // This "looks" like a hex-float so treat it as one.
+  bool seen_p = false;
+  bool seen_dot = false;
+  uint_type fraction_index = 0;
+
+  uint_type fraction = 0;
+  int_type exponent = HF::exponent_bias;
+
+  // Strip off leading zeros so we don't have to special-case them later.
+  while ((next_char = is.peek()) == '0') {
+    is.get();
+  }
+
+  bool is_denorm =
+      true;  // Assume denorm "representation" until we hear otherwise.
+             // NB: This does not mean the value is actually denorm,
+             // it just means that it was written 0.
+  bool bits_written = false;  // Stays false until we write a bit.
+  while (!seen_p && !seen_dot) {
+    // Handle characters that are left of the fractional part.
+    if (next_char == '.') {
+      seen_dot = true;
+    } else if (next_char == 'p') {
+      seen_p = true;
+    } else if (::isxdigit(next_char)) {
+      // We know this is not denormalized since we have stripped all leading
+      // zeroes and we are not a ".".
+      is_denorm = false;
+      uint8_t number = get_nibble_from_character(next_char);
+      for (int i = 0; i < 4; ++i, number <<= 1) {
+        uint_type write_bit = (number & 0x8) ? 0x1 : 0x0;
+        if (bits_written) {
+          // If we are here the bits represented belong in the fractional
+          // part of the float, and we have to adjust the exponent accordingly.
+          fraction |= write_bit << (HF::top_bit_left_shift - fraction_index++);
+          exponent += 1;
+        }
+        bits_written |= write_bit;
+      }
+    } else {
+      // We have not found our exponent yet, so we have to fail.
+      is.setstate(std::ios::failbit);
+      return is;
+    }
+    is.get();
+    next_char = is.peek();
+  }
+  bits_written = false;
+  while (seen_dot && !seen_p) {
+    // Handle only fractional parts now.
+    if (next_char == 'p') {
+      seen_p = true;
+    } else if (::isxdigit(next_char)) {
+      int number = get_nibble_from_character(next_char);
+      for (int i = 0; i < 4; ++i, number <<= 1) {
+        uint_type write_bit = (number & 0x8) ? 0x01 : 0x00;
+        bits_written |= write_bit;
+        if (is_denorm && !bits_written) {
+          // Handle modifying the exponent here this way we can handle
+          // an arbitrary number of hex values without overflowing our
+          // integer.
+          exponent -= 1;
+        } else {
+          fraction |= write_bit << (HF::top_bit_left_shift - fraction_index++);
+        }
+      }
+    } else {
+      // We still have not found our 'p' exponent yet, so this is not a valid
+      // hex-float.
+      is.setstate(std::ios::failbit);
+      return is;
+    }
+    is.get();
+    next_char = is.peek();
+  }
+
+  bool seen_sign = false;
+  int8_t exponent_sign = 1;
+  int_type written_exponent = 0;
+  while (true) {
+    if ((next_char == '-' || next_char == '+')) {
+      if (seen_sign) {
+        is.setstate(std::ios::failbit);
+        return is;
+      }
+      seen_sign = true;
+      exponent_sign = (next_char == '-') ? -1 : 1;
+    } else if (::isdigit(next_char)) {
+      // Hex-floats express their exponent as decimal.
+      written_exponent *= 10;
+      written_exponent += next_char - '0';
+    } else {
+      break;
+    }
+    is.get();
+    next_char = is.peek();
+  }
+
+  written_exponent *= exponent_sign;
+  exponent += written_exponent;
+
+  bool is_zero = is_denorm && (fraction == 0);
+  if (is_denorm && !is_zero) {
+    fraction <<= 1;
+    exponent -= 1;
+  } else if (is_zero) {
+    exponent = 0;
+  }
+
+  if (exponent <= 0 && !is_zero) {
+    fraction >>= 1;
+    fraction |= static_cast<uint_type>(1) << HF::top_bit_left_shift;
+  }
+
+  fraction = (fraction >> HF::fraction_right_shift) & HF::fraction_encode_mask;
+
+  const uint_type max_exponent =
+      SetBits<uint_type, 0, HF::num_exponent_bits>::get;
+
+  // Handle actual denorm numbers
+  while (exponent < 0 && !is_zero) {
+    fraction >>= 1;
+    exponent += 1;
+
+    fraction &= HF::fraction_encode_mask;
+    if (fraction == 0) {
+      // We have underflowed our fraction. We should clamp to zero.
+      is_zero = true;
+      exponent = 0;
+    }
+  }
+
+  // We have overflowed so we should be inf/-inf.
+  if (exponent > max_exponent) {
+    exponent = max_exponent;
+    fraction = 0;
+  }
+
+  uint_type output_bits = static_cast<uint_type>(negate_value ? 1 : 0)
+                          << HF::top_bit_left_shift;
+  output_bits |= fraction;
+  output_bits |= (exponent << HF::exponent_left_shift) & HF::exponent_mask;
+
+  T output_float = spvutils::BitwiseCast<T>(output_bits);
+  value.set_value(output_float);
+
+  return is;
+}
 }
 
 #endif  // _LIBSPIRV_UTIL_HEX_FLOAT_H_
index a6cafa4..f80f9e1 100644 (file)
 
 namespace {
 using ::testing::Eq;
+using spvutils::BitwiseCast;
 
-using HexFloatEncodeTest =
+using HexFloatTest =
     ::testing::TestWithParam<std::pair<float, std::string>>;
+using DecodeHexFloatTest =
+    ::testing::TestWithParam<std::pair<std::string, float>>;
 
-TEST_P(HexFloatEncodeTest, EncodeCorrectly) {
+TEST_P(HexFloatTest, EncodeCorrectly) {
   std::stringstream ss;
   ss << spvutils::HexFloat<float>(std::get<0>(GetParam()));
   EXPECT_THAT(ss.str(), Eq(std::get<1>(GetParam())));
 }
 
+TEST_P(HexFloatTest, DecodeCorrectly) {
+  std::stringstream ss(std::get<1>(GetParam()));
+  spvutils::HexFloat<float> myFloat(0.f);
+  ss >> myFloat;
+  float expected = std::get<0>(GetParam());
+  // Check that the two floats are bitwise equal. We do it this way because
+  // nan != nan, and so the test would fail even if they were exactly the same.
+  EXPECT_THAT(BitwiseCast<uint32_t>(myFloat.value()),
+              Eq(BitwiseCast<uint32_t>(std::get<0>(GetParam()))));
+}
+
 INSTANTIATE_TEST_CASE_P(
-    Float32Tests, HexFloatEncodeTest,
+    Float32Tests, HexFloatTest,
     ::testing::ValuesIn(std::vector<std::pair<float, std::string>>({
         {0.f, "0x0p+0"},
         {1.f, "0x1p+0"},
@@ -78,40 +92,76 @@ INSTANTIATE_TEST_CASE_P(
         {1.0f / -1024.f - 1.0f / 8.f, "-0x1.02p-3"},
 
         // lowest non-denorm
-        {1.0 / (powf(2.0f, 126.0f)), "0x1p-126"},
-        {-1.0 / (powf(2.0f, 126.0f)), "-0x1p-126"},
+        {ldexp(1.0f, -126), "0x1p-126"},
+        {ldexp(-1.0f, -126), "-0x1p-126"},
 
         // Denormalized values
-        {1.0 / (powf(2.0f, 127.0f)), "0x1p-127"},
-        {(1.0 / (powf(2.0f, 127.0f))) / 2.0f, "0x1p-128"},
-        {(1.0 / (powf(2.0f, 127.0f))) / 4.0f, "0x1p-129"},
-        {(1.0 / (powf(2.0f, 127.0f))) / 8.0f, "0x1p-130"},
-        {-1.0 / (powf(2.0f, 127.0f)), "-0x1p-127"},
-        {(-1.0 / (powf(2.0f, 127.0f))) / 2.0f, "-0x1p-128"},
-        {(-1.0 / (powf(2.0f, 127.0f))) / 4.0f, "-0x1p-129"},
-        {(-1.0 / (powf(2.0f, 127.0f))) / 8.0f, "-0x1p-130"},
-
-        {(1.0 / (powf(2.0f, 127.0f))) +
-          ((1.0 / (powf(2.0f, 127.0f))) / 2.0f), "0x1.8p-127"},
-        {(1.0 / (powf(2.0f, 127.0f)) / 2.0f) +
-          ((1.0 / (powf(2.0f, 127.0f))) / 4.0f), "0x1.8p-128"},
-
+        {ldexp(1.0f, -127), "0x1p-127"},
+        {ldexp(1.0f, -127) / 2.0f, "0x1p-128"},
+        {ldexp(1.0f, -127) / 4.0f, "0x1p-129"},
+        {ldexp(1.0f, -127) / 8.0f, "0x1p-130"},
+        {ldexp(-1.0f, -127), "-0x1p-127"},
+        {ldexp(-1.0f, -127) / 2.0f, "-0x1p-128"},
+        {ldexp(-1.0f, -127) / 4.0f, "-0x1p-129"},
+        {ldexp(-1.0f, -127) / 8.0f, "-0x1p-130"},
+
+        {ldexp(1.0, -127) + (ldexp(1.0, -127) / 2.0f), "0x1.8p-127"},
+        {ldexp(1.0, -127) / 2.0 + (ldexp(1.0, -127) / 4.0f), "0x1.8p-128"},
 
         // Various NAN and INF cases
-        {spvutils::BitwiseCast<float>(0xFF800000), "-0x1p+128"},         // -inf
-        {spvutils::BitwiseCast<float>(0x7F800000), "0x1p+128"},          // inf
-        {spvutils::BitwiseCast<float>(0xFF800000), "-0x1p+128"},         // -nan
-        {spvutils::BitwiseCast<float>(0xFF800100), "-0x1.0002p+128"},    // -nan
-        {spvutils::BitwiseCast<float>(0xFF800c00), "-0x1.0018p+128"},    // -nan
-        {spvutils::BitwiseCast<float>(0xFF80F000), "-0x1.01ep+128"},     // -nan
-        {spvutils::BitwiseCast<float>(0xFFFFFFFF), "-0x1.fffffep+128"},  // -nan
-        {spvutils::BitwiseCast<float>(0x7F800000), "0x1p+128"},          // +nan
-        {spvutils::BitwiseCast<float>(0x7F800100), "0x1.0002p+128"},     // +nan
-        {spvutils::BitwiseCast<float>(0x7F800c00), "0x1.0018p+128"},     // +nan
-        {spvutils::BitwiseCast<float>(0x7F80F000), "0x1.01ep+128"},      // +nan
-        {spvutils::BitwiseCast<float>(0x7FFFFFFF), "0x1.fffffep+128"},   // +nan
+        {BitwiseCast<float>(0xFF800000), "-0x1p+128"},         // -inf
+        {BitwiseCast<float>(0x7F800000), "0x1p+128"},          // inf
+        {BitwiseCast<float>(0xFFC00000), "-0x1.8p+128"},       // -nan
+        {BitwiseCast<float>(0xFF800100), "-0x1.0002p+128"},    // -nan
+        {BitwiseCast<float>(0xFF800c00), "-0x1.0018p+128"},    // -nan
+        {BitwiseCast<float>(0xFF80F000), "-0x1.01ep+128"},     // -nan
+        {BitwiseCast<float>(0xFFFFFFFF), "-0x1.fffffep+128"},  // -nan
+        {BitwiseCast<float>(0x7FC00000), "0x1.8p+128"},        // +nan
+        {BitwiseCast<float>(0x7F800100), "0x1.0002p+128"},     // +nan
+        {BitwiseCast<float>(0x7f800c00), "0x1.0018p+128"},     // +nan
+        {BitwiseCast<float>(0x7F80F000), "0x1.01ep+128"},      // +nan
+        {BitwiseCast<float>(0x7FFFFFFF), "0x1.fffffep+128"},   // +nan
     })));
 
+TEST_P(DecodeHexFloatTest, DecodeCorrectly) {
+  std::stringstream ss(std::get<0>(GetParam()));
+  spvutils::HexFloat<float> myFloat(0.f);
+  ss >> myFloat;
+  float expected = std::get<1>(GetParam());
+  // Check that the two floats are bitwise equal. We do it this way because
+  // nan != nan, and so the test would fail even if they were exactly the same.
+  EXPECT_THAT(BitwiseCast<uint32_t>(myFloat.value()),
+              Eq(BitwiseCast<uint32_t>(std::get<1>(GetParam()))));
+}
+
+INSTANTIATE_TEST_CASE_P(
+    Float32DecodeTests, DecodeHexFloatTest,
+    ::testing::ValuesIn(std::vector<std::pair<std::string, float>>({
+        {"0x0p+000", 0.f},
+        {"0x0p0", 0.f},
+        {"0x0p-0", 0.f},
+
+        // inf cases
+        {"-0x1p+128", BitwiseCast<float>(0xFF800000)},         // -inf
+        {"0x32p+127", BitwiseCast<float>(0x7F800000)},         // inf
+        {"0x32p+500", BitwiseCast<float>(0x7F800000)},         // inf
+        {"-0x32p+127", BitwiseCast<float>(0xFF800000)},         // -inf
+
+        // flush to zero cases
+        {"0x1p-500", 0.f}, // Exponent underflows.
+        {"-0x1p-500", -0.f},
+        {"0x0.00000000001p-126", 0.f}, // Fraction causes underfloat.
+        {"-0x0.0000000001p-127", -0.f},
+        {"-0x0.01p-142", -0.f}, // Fraction causes undeflow to underflow more.
+        {"0x0.01p-142", 0.f},
+
+        // Some floats that do not encode the same way as the decode.
+        {"0x2p+0", 2.f},
+        {"0xFFp+0", 255.f},
+        {"0x0.8p+0", 0.5f},
+        {"0x0.4p+0", 0.25f},
+    })));
+// TODO(awoloszyn): Add some more decoding tests with "abnormal" formats.
 // TODO(awoloszyn): Add double tests
 // TODO(awoloszyn): Add fp16 tests and HexFloatTraits.