Fix FloatToString printing with not enough precision when using `fixed` floatfield format

Setting precision to `std::numeric_limits<float>::max_digits10` is valid
when using the `scientific` floatfield format when printing values.
However, we used `fixed` to make our floats more human-readable. This
change keeps the output in `fixed`, except if doing so loses precision,
in which case we fall back to `scientific`.

This fixes the rendering differences seen in the Babylon.js examples
(https://crbug.com/tint/944) between Dawn using Tint vs SPIRV-Cross, as
Tint's output was emitting values that had lost too much precision
(e.g. very small numbers being output as 0).

Bug: tint:944
Change-Id: I8deea23ad876825bbe390fc26907d4bbbd4b966e
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/58321
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/writer/float_to_string.cc b/src/writer/float_to_string.cc
index 81e66bc..900599d 100644
--- a/src/writer/float_to_string.cc
+++ b/src/writer/float_to_string.cc
@@ -19,6 +19,7 @@
 #include <iomanip>
 #include <limits>
 #include <sstream>
+#include <functional>
 
 #include "src/debug.h"
 
@@ -26,16 +27,31 @@
 namespace writer {
 
 std::string FloatToString(float f) {
-  std::stringstream ss;
-  ss.flags(ss.flags() | std::ios_base::showpoint | std::ios_base::fixed);
-  ss.precision(std::numeric_limits<float>::max_digits10);
-  ss << f;
-  auto str = ss.str();
-  while (str.length() >= 2 && str[str.size() - 1] == '0' &&
-         str[str.size() - 2] != '.') {
-    str.pop_back();
+  // Try printing the float in fixed point, with a smallish limit on the
+  // precision
+  std::stringstream fixed;
+  fixed.flags(fixed.flags() | std::ios_base::showpoint | std::ios_base::fixed);
+  fixed.precision(9);
+  fixed << f;
+
+  // If this string can be parsed without loss of information, use it
+  auto float_equal_no_warning = std::equal_to<float>();
+  if (float_equal_no_warning(std::stof(fixed.str()), f)) {
+    auto str = fixed.str();
+    while (str.length() >= 2 && str[str.size() - 1] == '0' &&
+           str[str.size() - 2] != '.') {
+      str.pop_back();
+    }
+
+    return str;
   }
-  return str;
+
+  // Resort to scientific, with the minimum precision needed to preserve the
+  // whole float
+  std::stringstream sci;
+  sci.precision(std::numeric_limits<float>::max_digits10);
+  sci << f;
+  return sci.str();
 }
 
 std::string FloatToBitPreservingString(float f) {
diff --git a/src/writer/float_to_string_test.cc b/src/writer/float_to_string_test.cc
index 4a0f64c..2ff278e 100644
--- a/src/writer/float_to_string_test.cc
+++ b/src/writer/float_to_string_test.cc
@@ -88,6 +88,13 @@
             "-340282346638528859811704183484516925440.0");
 }
 
+TEST(FloatToStringTest, Precision) {
+  EXPECT_EQ(FloatToString(1e-8f), "0.00000001");
+  EXPECT_EQ(FloatToString(1e-9f), "0.000000001");
+  EXPECT_EQ(FloatToString(1e-10f), "1.00000001e-10");
+  EXPECT_EQ(FloatToString(1e-20f), "9.99999968e-21");
+}
+
 // FloatToBitPreservingString
 //
 // First replicate the tests for FloatToString