wsgl-writer: emit inf, nan, subnormal as hex float

Signed zeros are emitted.
Subormal numbers are emitted as hex float.

Handling Inf and NaN is unresolved in the spec.
See https://github.com/gpuweb/gpuweb/issues/1769

NaN tests are disabled, due to platform dependence.
Windows x86-64 seems to always set the high mantissa bit
on NaNs.

Fixed: tint:76
Change-Id: I06c69b93b04c869a63ec2f574022996acc7a6dbe
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/52321
Commit-Queue: David Neto <dneto@google.com>
Auto-Submit: David Neto <dneto@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index bed5807..a1c6a05 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -789,6 +789,7 @@
       writer/wgsl/generator_impl_identifier_test.cc
       writer/wgsl/generator_impl_if_test.cc
       writer/wgsl/generator_impl_loop_test.cc
+      writer/wgsl/generator_impl_literal_test.cc
       writer/wgsl/generator_impl_member_accessor_test.cc
       writer/wgsl/generator_impl_return_test.cc
       writer/wgsl/generator_impl_switch_test.cc
diff --git a/src/writer/float_to_string.cc b/src/writer/float_to_string.cc
index b179ed0..1692f26 100644
--- a/src/writer/float_to_string.cc
+++ b/src/writer/float_to_string.cc
@@ -14,9 +14,14 @@
 
 #include "src/writer/float_to_string.h"
 
+#include <cmath>
+#include <cstring>
+#include <iomanip>
 #include <limits>
 #include <sstream>
 
+#include "src/debug.h"
+
 namespace tint {
 namespace writer {
 
@@ -33,5 +38,105 @@
   return str;
 }
 
+std::string FloatToBitPreservingString(float f) {
+  // For the NaN case, avoid handling the number as a floating point value.
+  // Some machines will modify the top bit in the mantissa of a NaN.
+
+  std::stringstream ss;
+
+  uint32_t float_bits = 0u;
+  std::memcpy(&float_bits, &f, sizeof(float_bits));
+
+  // Handle the sign.
+  const uint32_t kSignMask = 1u << 31;
+  if (float_bits & kSignMask) {
+    // If `f` is -0.0 print -0.0.
+    ss << '-';
+    // Strip sign bit.
+    float_bits = float_bits & (~kSignMask);
+  }
+
+  switch (std::fpclassify(f)) {
+    case FP_ZERO:
+    case FP_NORMAL:
+      std::memcpy(&f, &float_bits, sizeof(float_bits));
+      ss << FloatToString(f);
+      break;
+
+    default: {
+      // Infinity, NaN, and Subnormal
+      // TODO(dneto): It's unclear how Infinity and NaN should be handled.
+      // See https://github.com/gpuweb/gpuweb/issues/1769
+
+      // std::hexfloat prints 'nan' and 'inf' instead of an
+      // explicit representation like we want. Split it out
+      // manually.
+      const int kExponentBias = 127;
+      const int kExponentMask = 0x7f800000;
+      const int kMantissaMask = 0x007fffff;
+      const int kMantissaBits = 23;
+
+      int mantissaNibbles = (kMantissaBits + 3) / 4;
+
+      const int biased_exponent =
+          static_cast<int>((float_bits & kExponentMask) >> kMantissaBits);
+      int exponent = biased_exponent - kExponentBias;
+      uint32_t mantissa = float_bits & kMantissaMask;
+
+      ss << "0x";
+
+      if (exponent == 128) {
+        if (mantissa == 0) {
+          //  Infinity case.
+          ss << "1p+128";
+        } else {
+          //  NaN case.
+          //  Emit the mantissa bits as if they are left-justified after the
+          //  binary point.  This is what SPIRV-Tools hex float emitter does,
+          //  and it's a justifiable choice independent of the bit width
+          //  of the mantissa.
+          mantissa <<= (4 - (kMantissaBits % 4));
+          // Remove trailing zeroes, for tidyness.
+          while (0 == (0xf & mantissa)) {
+            mantissa >>= 4;
+            mantissaNibbles--;
+          }
+          ss << "1." << std::hex << std::setfill('0')
+             << std::setw(mantissaNibbles) << mantissa << "p+128";
+        }
+      } else {
+        // Subnormal, and not zero.
+        TINT_ASSERT(mantissa != 0);
+        const int kTopBit = (1 << kMantissaBits);
+
+        // Shift left until we get 1.x
+        while (0 == (kTopBit & mantissa)) {
+          mantissa <<= 1;
+          exponent--;
+        }
+        // Emit the leading 1, and remove it from the mantissa.
+        ss << "1";
+        mantissa = mantissa ^ kTopBit;
+        mantissa <<= 1;
+        exponent++;
+
+        // Emit the fractional part.
+        if (mantissa) {
+          // Remove trailing zeroes, for tidyness
+          while (0 == (0xf & mantissa)) {
+            mantissa >>= 4;
+            mantissaNibbles--;
+          }
+          ss << "." << std::hex << std::setfill('0')
+             << std::setw(mantissaNibbles) << mantissa;
+        }
+        // Emit the exponent
+        ss << "p" << std::showpos << std::dec << exponent;
+      }
+    }
+  }
+  return ss.str();
+}
+
 }  // namespace writer
 }  // namespace tint
diff --git a/src/writer/float_to_string.h b/src/writer/float_to_string.h
index cd29134..9f385db 100644
--- a/src/writer/float_to_string.h
+++ b/src/writer/float_to_string.h
@@ -28,6 +28,11 @@
 /// @return the float f formatted to a string
 std::string FloatToString(float f);
 
+/// Converts the float `f` to a string, using hex float notation for infinities,
+/// NaNs, or subnormal numbers. Otherwise behaves as FloatToString.
+/// @return the float f formatted to a string
+std::string FloatToBitPreservingString(float f);
+
 }  // namespace writer
 }  // namespace tint
 
diff --git a/src/writer/float_to_string_test.cc b/src/writer/float_to_string_test.cc
index dda4554..4a0f64c 100644
--- a/src/writer/float_to_string_test.cc
+++ b/src/writer/float_to_string_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/writer/float_to_string.h"
 
+#include <cmath>
+#include <cstring>
 #include <limits>
 
 #include "gtest/gtest.h"
@@ -22,6 +24,25 @@
 namespace writer {
 namespace {
 
+// Makes an IEEE 754 binary32 floating point number with
+// - 0 sign if sign is 0, 1 otherwise
+// - 'exponent_bits' is placed in the exponent space.
+//   So, the exponent bias must already be included.
+float MakeFloat(int sign, int biased_exponent, int mantissa) {
+  const uint32_t sign_bit = sign ? 0x80000000u : 0u;
+  // The binary32 exponent is 8 bits, just below the sign.
+  const uint32_t exponent_bits = (biased_exponent & 0xffu) << 23;
+  // The mantissa is the bottom 23 bits.
+  const uint32_t mantissa_bits = (mantissa & 0x7fffffu);
+
+  uint32_t bits = sign_bit | exponent_bits | mantissa_bits;
+  float result = 0.0f;
+  static_assert(sizeof(result) == sizeof(bits),
+                "expected float and uint32_t to be the same size");
+  std::memcpy(&result, &bits, sizeof(bits));
+  return result;
+}
+
 TEST(FloatToStringTest, Zero) {
   EXPECT_EQ(FloatToString(0.0f), "0.0");
 }
@@ -67,6 +88,132 @@
             "-340282346638528859811704183484516925440.0");
 }
 
+// FloatToBitPreservingString
+//
+// First replicate the tests for FloatToString
+
+TEST(FloatToBitPreservingStringTest, Zero) {
+  EXPECT_EQ(FloatToBitPreservingString(0.0f), "0.0");
+}
+
+TEST(FloatToBitPreservingStringTest, One) {
+  EXPECT_EQ(FloatToBitPreservingString(1.0f), "1.0");
+}
+
+TEST(FloatToBitPreservingStringTest, MinusOne) {
+  EXPECT_EQ(FloatToBitPreservingString(-1.0f), "-1.0");
+}
+
+TEST(FloatToBitPreservingStringTest, Billion) {
+  EXPECT_EQ(FloatToBitPreservingString(1e9f), "1000000000.0");
+}
+
+TEST(FloatToBitPreservingStringTest, Small) {
+  EXPECT_NE(FloatToBitPreservingString(std::numeric_limits<float>::epsilon()),
+            "0.0");
+}
+
+TEST(FloatToBitPreservingStringTest, Highest) {
+  const auto highest = std::numeric_limits<float>::max();
+  const auto expected_highest = 340282346638528859811704183484516925440.0f;
+  if (highest < expected_highest || highest > expected_highest) {
+    GTEST_SKIP() << "std::numeric_limits<float>::max() is not as expected for "
+                    "this target";
+  }
+  EXPECT_EQ(FloatToBitPreservingString(std::numeric_limits<float>::max()),
+            "340282346638528859811704183484516925440.0");
+}
+
+TEST(FloatToBitPreservingStringTest, Lowest) {
+  // Some compilers complain if you test floating point numbers for equality.
+  // So say it via two inequalities.
+  const auto lowest = std::numeric_limits<float>::lowest();
+  const auto expected_lowest = -340282346638528859811704183484516925440.0f;
+  if (lowest < expected_lowest || lowest > expected_lowest) {
+    GTEST_SKIP()
+        << "std::numeric_limits<float>::lowest() is not as expected for "
+           "this target";
+  }
+  EXPECT_EQ(FloatToBitPreservingString(std::numeric_limits<float>::lowest()),
+            "-340282346638528859811704183484516925440.0");
+}
+
+// Special cases for bit-preserving output.
+
+TEST(FloatToBitPreservingStringTest, NegativeZero) {
+  EXPECT_EQ(FloatToBitPreservingString(std::copysign(0.0f, -5.0f)), "-0.0");
+}
+
+TEST(FloatToBitPreservingStringTest, ZeroAsBits) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0)), "0.0");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0)), "-0.0");
+}
+
+TEST(FloatToBitPreservingStringTest, OneBits) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 127, 0)), "1.0");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 127, 0)), "-1.0");
+}
+
+TEST(FloatToBitPreservingStringTest, SmallestDenormal) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 1)), "0x1p-149");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 1)), "-0x1p-149");
+}
+
+TEST(FloatToBitPreservingStringTest, BiggerDenormal) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 2)), "0x1p-148");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 2)), "-0x1p-148");
+}
+
+TEST(FloatToBitPreservingStringTest, LargestDenormal) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0x7fffff)),
+            "0x1.fffffcp-127");
+}
+
+TEST(FloatToBitPreservingStringTest, Subnormal_cafebe) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0xcafebe)),
+            "0x1.2bfaf8p-127");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0xcafebe)),
+            "-0x1.2bfaf8p-127");
+}
+
+TEST(FloatToBitPreservingStringTest, Subnormal_aaaaa) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0xaaaaa)),
+            "0x1.55554p-130");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0xaaaaa)),
+            "-0x1.55554p-130");
+}
+
+TEST(FloatToBitPreservingStringTest, Infinity) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0)), "0x1p+128");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0)), "-0x1p+128");
+}
+
+// TODO(dneto): It's unclear how Infinity and NaN should be handled.
+// https://github.com/gpuweb/gpuweb/issues/1769
+// Windows x86-64 sets the high mantissa bit on NaNs.
+// Disable NaN tests for now.
+
+TEST(FloatToBitPreservingStringTest, DISABLED_NaN_MsbOnly) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0x400000)),
+            "0x1.8p+128");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0x400000)),
+            "-0x1.8p+128");
+}
+
+TEST(FloatToBitPreservingStringTest, DISABLED_NaN_LsbOnly) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0x1)),
+            "0x1.000002p+128");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0x1)),
+            "-0x1.000002p+128");
+}
+
+TEST(FloatToBitPreservingStringTest, DISABLED_NaN_NonMsb) {
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0x20101f)),
+            "0x1.40203ep+128");
+  EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0x20101f)),
+            "-0x1.40203ep+128");
+}
+
 }  // namespace
 }  // namespace writer
 }  // namespace tint
diff --git a/src/writer/wgsl/generator_impl.cc b/src/writer/wgsl/generator_impl.cc
index ef96c53..17b8359 100644
--- a/src/writer/wgsl/generator_impl.cc
+++ b/src/writer/wgsl/generator_impl.cc
@@ -253,7 +253,7 @@
   if (auto* bl = lit->As<ast::BoolLiteral>()) {
     out_ << (bl->IsTrue() ? "true" : "false");
   } else if (auto* fl = lit->As<ast::FloatLiteral>()) {
-    out_ << FloatToString(fl->value());
+    out_ << FloatToBitPreservingString(fl->value());
   } else if (auto* sl = lit->As<ast::SintLiteral>()) {
     out_ << sl->value();
   } else if (auto* ul = lit->As<ast::UintLiteral>()) {
diff --git a/src/writer/wgsl/generator_impl_literal_test.cc b/src/writer/wgsl/generator_impl_literal_test.cc
new file mode 100644
index 0000000..6ded4c8
--- /dev/null
+++ b/src/writer/wgsl/generator_impl_literal_test.cc
@@ -0,0 +1,132 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstring>
+
+#include "src/ast/override_decoration.h"
+#include "src/writer/wgsl/test_helper.h"
+
+namespace tint {
+namespace writer {
+namespace wgsl {
+namespace {
+
+// Makes an IEEE 754 binary32 floating point number with
+// - 0 sign if sign is 0, 1 otherwise
+// - 'exponent_bits' is placed in the exponent space.
+//   So, the exponent bias must already be included.
+float MakeFloat(int sign, int biased_exponent, int mantissa) {
+  const uint32_t sign_bit = sign ? 0x80000000u : 0u;
+  // The binary32 exponent is 8 bits, just below the sign.
+  const uint32_t exponent_bits = (biased_exponent & 0xffu) << 23;
+  // The mantissa is the bottom 23 bits.
+  const uint32_t mantissa_bits = (mantissa & 0x7fffffu);
+
+  uint32_t bits = sign_bit | exponent_bits | mantissa_bits;
+  float result = 0.0f;
+  static_assert(sizeof(result) == sizeof(bits),
+                "expected float and uint32_t to be the same size");
+  std::memcpy(&result, &bits, sizeof(bits));
+  return result;
+}
+
+struct FloatData {
+  float value;
+  std::string expected;
+};
+inline std::ostream& operator<<(std::ostream& out, FloatData data) {
+  out << "{" << data.value << "," << data.expected << "}";
+  return out;
+}
+
+using WgslGenerator_FloatLiteralTest = TestParamHelper<FloatData>;
+
+TEST_P(WgslGenerator_FloatLiteralTest, Emit) {
+  auto* v = Expr(GetParam().value);
+
+  SetResolveOnBuild(false);
+  GeneratorImpl& gen = Build();
+
+  ASSERT_TRUE(gen.EmitScalarConstructor(v)) << gen.error();
+  EXPECT_EQ(gen.result(), GetParam().expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(Zero,
+                         WgslGenerator_FloatLiteralTest,
+                         ::testing::ValuesIn(std::vector<FloatData>{
+                             {0.0f, "0.0"},
+                             {MakeFloat(0, 0, 0), "0.0"},
+                             {MakeFloat(1, 0, 0), "-0.0"}}));
+
+INSTANTIATE_TEST_SUITE_P(Normal,
+                         WgslGenerator_FloatLiteralTest,
+                         ::testing::ValuesIn(std::vector<FloatData>{
+                             {1.0f, "1.0"},
+                             {-1.0f, "-1.0"},
+                             {101.375, "101.375"}}));
+
+INSTANTIATE_TEST_SUITE_P(
+    Subnormal,
+    WgslGenerator_FloatLiteralTest,
+    ::testing::ValuesIn(std::vector<FloatData>{
+        {MakeFloat(0, 0, 1), "0x1p-149"},  // Smallest
+        {MakeFloat(1, 0, 1), "-0x1p-149"},
+        {MakeFloat(0, 0, 2), "0x1p-148"},
+        {MakeFloat(1, 0, 2), "-0x1p-148"},
+        {MakeFloat(0, 0, 0x7fffff), "0x1.fffffcp-127"},   // Largest
+        {MakeFloat(1, 0, 0x7fffff), "-0x1.fffffcp-127"},  // Largest
+        {MakeFloat(0, 0, 0xcafebe), "0x1.2bfaf8p-127"},   // Scattered bits
+        {MakeFloat(1, 0, 0xcafebe), "-0x1.2bfaf8p-127"},  // Scattered bits
+        {MakeFloat(0, 0, 0xaaaaa), "0x1.55554p-130"},     // Scattered bits
+        {MakeFloat(1, 0, 0xaaaaa), "-0x1.55554p-130"},    // Scattered bits
+    }));
+
+INSTANTIATE_TEST_SUITE_P(Infinity,
+                         WgslGenerator_FloatLiteralTest,
+                         ::testing::ValuesIn(std::vector<FloatData>{
+                             {MakeFloat(0, 255, 0), "0x1p+128"},
+                             {MakeFloat(1, 255, 0), "-0x1p+128"}}));
+
+INSTANTIATE_TEST_SUITE_P(
+    // TODO(dneto): It's unclear how Infinity and NaN should be handled.
+    // https://github.com/gpuweb/gpuweb/issues/1769
+    // This test fails on Windows x86-64 because the machine sets the high
+    // mantissa bit on NaNs.
+    DISABLED_NaN,
+    // In the NaN case, the top bit in the mantissa is often used to encode
+    // whether the NaN is signalling or quiet, but no agreement between
+    // different machine architectures on whether 1 means signalling or
+    // if 1 means quiet.
+    WgslGenerator_FloatLiteralTest,
+    ::testing::ValuesIn(std::vector<FloatData>{
+        // LSB only.  Smallest mantissa.
+        {MakeFloat(0, 255, 1), "0x1.000002p+128"},  // Smallest mantissa
+        {MakeFloat(1, 255, 1), "-0x1.000002p+128"},
+        // MSB only.
+        {MakeFloat(0, 255, 0x400000), "0x1.8p+128"},
+        {MakeFloat(1, 255, 0x400000), "-0x1.8p+128"},
+        // All 1s in the mantissa.
+        {MakeFloat(0, 255, 0x7fffff), "0x1.fffffep+128"},
+        {MakeFloat(1, 255, 0x7fffff), "-0x1.fffffep+128"},
+        // Scattered bits, with 0 in top mantissa bit.
+        {MakeFloat(0, 255, 0x20101f), "0x1.40203ep+128"},
+        {MakeFloat(1, 255, 0x20101f), "-0x1.40203ep+128"},
+        // Scattered bits, with 1 in top mantissa bit.
+        {MakeFloat(0, 255, 0x40101f), "0x1.80203ep+128"},
+        {MakeFloat(1, 255, 0x40101f), "-0x1.80203ep+128"}}));
+
+}  // namespace
+}  // namespace wgsl
+}  // namespace writer
+}  // namespace tint
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 1ea5ae7..30cc5db 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -258,9 +258,9 @@
     "../src/resolver/intrinsic_test.cc",
     "../src/resolver/is_host_shareable_test.cc",
     "../src/resolver/is_storeable_test.cc",
+    "../src/resolver/pipeline_overridable_constant_test.cc",
     "../src/resolver/ptr_ref_test.cc",
     "../src/resolver/ptr_ref_validation_test.cc",
-    "../src/resolver/pipeline_overridable_constant_test.cc",
     "../src/resolver/resolver_test.cc",
     "../src/resolver/resolver_test_helper.cc",
     "../src/resolver/resolver_test_helper.h",
@@ -511,6 +511,7 @@
     "../src/writer/wgsl/generator_impl_global_decl_test.cc",
     "../src/writer/wgsl/generator_impl_identifier_test.cc",
     "../src/writer/wgsl/generator_impl_if_test.cc",
+    "../src/writer/wgsl/generator_impl_literal_test.cc",
     "../src/writer/wgsl/generator_impl_loop_test.cc",
     "../src/writer/wgsl/generator_impl_member_accessor_test.cc",
     "../src/writer/wgsl/generator_impl_return_test.cc",