Skip to content

[clang][APFloat] Print floating-point literals in the shortest round-trip form - #218471

Open
conrade-ctc wants to merge 1 commit into
llvm:mainfrom
chicagotrading:apfloat-tostring-shortest
Open

[clang][APFloat] Print floating-point literals in the shortest round-trip form#218471
conrade-ctc wants to merge 1 commit into
llvm:mainfrom
chicagotrading:apfloat-tostring-shortest

Conversation

@conrade-ctc

@conrade-ctc conrade-ctc commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

StmtPrinter printed a FloatingLiteral at the maximum round-trip precision, so 3.14 printed as 3.1400000000000001. This patch adds APFloat::toStringShortest and calls it from PrintFloatingLiteral. NaN, infinity, and zero keep the old path.

toStringShortest computes the shortest digit count from the value's rounding interval (Ryu's formulation with a Dragonbox-style grid anchor) in exact APInt arithmetic, so every IEEEFloat semantics works. The existing toString then formats at that count and the round trip is verified. PPCDoubleDouble and the largest value of saturating formats keep a plain probe from precision 1.

The verify step is a new APFloat::toStringRoundTrip: format at a given precision and report whether the string parses back bitwise-equal. The MLIR and llubi printers hand-rolled the same check and now call the helper, with no output change. The IR AsmWriter keeps its numeric parse-back comparison: for PPCDoubleDouble, numerically equal is not bitwise equal (the low component can be -0.0).

Three tests pinned the old maximum-precision output and were updated. A new -ast-print test covers double/float/x87/quad, denormals, and a full-precision value; unit tests cover both helpers.

🤖 Done with the help of Claude Code (Fable 5, human in the loop)

@llvmorg-github-actions llvmorg-github-actions Bot added clang:frontend Language frontend issues, e.g. anything involving "Sema" mlir:core MLIR Core Infrastructure mlir llvm:support llvm:ir llvm:adt tools:llubi labels Aug 24, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-mlir-core
@llvm/pr-subscribers-mlir

@llvm/pr-subscribers-llvm-support

Author: Emery Conrad (conrade-ctc)

Changes

StmtPrinter printed a FloatingLiteral at the maximum round-trip precision, so 3.14 printed as 3.1400000000000001. This patch adds APFloat::toStringShortest, which probes increasing precisions and keeps the first string that parses back bitwise-equal, and calls it from PrintFloatingLiteral. NaN, infinity, and zero keep the old path. Values that need full precision (e.g. DBL_MAX) print as before.

The probe step is a new APFloat::toStringRoundTrip: format at a given precision and report whether the string parses back bitwise-equal. The IR, MLIR, and llubi printers hand-rolled the same check and now call the helper, with no output change there. A future shortest-digits algorithm has a single landing spot.

Three tests pinned the old maximum-precision output and were updated. A new -ast-print test covers double/float/x87/quad, denormals, and a full-precision value; new unit tests cover both helpers.

🤖 Done with the help of Claude Code (Fable 5, human in the loop)


Full diff: https://github.com/llvm/llvm-project/pull/218471.diff

12 Files Affected:

  • (modified) clang/docs/ReleaseNotes.md (+4)
  • (modified) clang/lib/AST/StmtPrinter.cpp (+1-1)
  • (added) clang/test/AST/ast-print-float-shortest.cpp (+41)
  • (modified) clang/test/PCH/floating-literal.c (+2-2)
  • (modified) clang/test/PCH/objc_literals.m (+3-3)
  • (modified) clang/test/Sema/x86_64-linux-android.c (+1-1)
  • (modified) llvm/include/llvm/ADT/APFloat.h (+14)
  • (modified) llvm/lib/IR/AsmWriter.cpp (+2-2)
  • (modified) llvm/lib/Support/APFloat.cpp (+38)
  • (modified) llvm/tools/llubi/lib/Value.cpp (+3-3)
  • (modified) llvm/unittests/ADT/APFloatTest.cpp (+68)
  • (modified) mlir/lib/IR/AsmPrinter.cpp (+7-5)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index ca0dbfa2af229..078d0a74bbf4b 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -98,6 +98,10 @@ features cannot lower the translation-unit ABI level;
 
 ### AST Dumping Potentially Breaking Changes
 
+- The AST printer (`-ast-print`) now prints a floating-point literal as the
+  shortest decimal form that round-trips to the same value. For example,
+  `3.14` printed as `3.1400000000000001` before and prints as `3.14` now.
+
 ### Clang Frontend Potentially Breaking Changes
 
 - Templight support has been removed.
diff --git a/clang/lib/AST/StmtPrinter.cpp b/clang/lib/AST/StmtPrinter.cpp
index e957b03b7e898..30df61440e15f 100644
--- a/clang/lib/AST/StmtPrinter.cpp
+++ b/clang/lib/AST/StmtPrinter.cpp
@@ -1594,7 +1594,7 @@ void StmtPrinter::VisitFixedPointLiteral(FixedPointLiteral *Node) {
 static void PrintFloatingLiteral(raw_ostream &OS, FloatingLiteral *Node,
                                  bool PrintSuffix) {
   SmallString<16> Str;
-  Node->getValue().toString(Str);
+  Node->getValue().toStringShortest(Str);
   OS << Str;
   if (Str.find_first_not_of("-0123456789") == StringRef::npos)
     OS << '.'; // Trailing dot in order to separate from ints.
diff --git a/clang/test/AST/ast-print-float-shortest.cpp b/clang/test/AST/ast-print-float-shortest.cpp
new file mode 100644
index 0000000000000..b5f21a7ae27ec
--- /dev/null
+++ b/clang/test/AST/ast-print-float-shortest.cpp
@@ -0,0 +1,41 @@
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -ast-print %s | FileCheck %s
+
+// Floating literals print as the shortest decimal form that round-trips.
+
+double d1 = 3.14;
+// CHECK: double d1 = 3.14;
+double d2 = 4.0;
+// CHECK: double d2 = 4.;
+double d3 = 0.5;
+// CHECK: double d3 = 0.5;
+double d4 = 1e10;
+// CHECK: double d4 = 1.0E+10;
+double d5 = 1e-300;
+// CHECK: double d5 = 1.0E-300;
+double d6 = 0.1;
+// CHECK: double d6 = 0.1;
+// The smallest double denormal: the shortest round-trip form is 5E-324.
+double d7 = 4.9406564584124654e-324;
+// CHECK: double d7 = 5.0E-324;
+// DBL_MAX needs all 17 significant digits; nothing shorter round-trips.
+double d8 = 1.7976931348623157e+308;
+// CHECK: double d8 = 1.7976931348623157E+308;
+double d9 = -3.14;
+// CHECK: double d9 = -3.14;
+
+float f1 = 3.14f;
+// CHECK: float f1 = 3.14F;
+float f2 = 0.1f;
+// CHECK: float f2 = 0.1F;
+// A hex float keeps its value, printed in decimal.
+float f3 = 0x1.8p3f;
+// CHECK: float f3 = 12.F;
+
+// x86 80-bit extended precision.
+long double ld1 = 0.1L;
+// CHECK: long double ld1 = 0.1L;
+long double ld2 = 3.14L;
+// CHECK: long double ld2 = 3.14L;
+
+__float128 q1 = 0.1q;
+// CHECK: __float128 q1 = 0.1Q;
diff --git a/clang/test/PCH/floating-literal.c b/clang/test/PCH/floating-literal.c
index b5ff6fe84b88c..4590b1990b79d 100644
--- a/clang/test/PCH/floating-literal.c
+++ b/clang/test/PCH/floating-literal.c
@@ -8,12 +8,12 @@
 // targets with 128-bit IEEE long doubles.
 
 long double foo = 1.0E4000L;
-// CHECK: long double foo = 1.00000000000000000000000000000000004E+4000L;
+// CHECK: long double foo = 1.0E+4000L;
 
 // Just as well check the others are still sane while we're here...
 
 double bar = 1.0E300;
-// CHECK: double bar = 1.0000000000000001E+300;
+// CHECK: double bar = 1.0E+300;
 
 float wibble = 1.0E40;
 // CHECK: float wibble = 1.0E+40;
diff --git a/clang/test/PCH/objc_literals.m b/clang/test/PCH/objc_literals.m
index 16ed6aeb9b933..ede3358e2fe69 100644
--- a/clang/test/PCH/objc_literals.m
+++ b/clang/test/PCH/objc_literals.m
@@ -45,18 +45,18 @@ static inline void test_numeric_literals(void) {
   // CHECK-PRINT: id intlit = @17
   // CHECK-IR: {{call.*17}}
   id intlit = @17;
-  // CHECK-PRINT: id floatlit = @17.449999999999999
+  // CHECK-PRINT: id floatlit = @17.45
   // CHECK-IR: {{call.*1.745}}
   id floatlit = @17.45;
 }
 
 static inline void test_array_literals(void) {
-  // CHECK-PRINT: id arraylit = @[ @17, @17.449999999999999
+  // CHECK-PRINT: id arraylit = @[ @17, @17.45
   id arraylit = @[@17, @17.45];
 }
 
 static inline void test_dictionary_literals(void) {
-  // CHECK-PRINT: id dictlit = @{ @17 : {{@17.449999999999999[^,]*}}, @"hello" : @"world" };
+  // CHECK-PRINT: id dictlit = @{ @17 : {{@17.45[^,]*}}, @"hello" : @"world" };
   id dictlit = @{@17 : @17.45, @"hello" : @"world" };
 }
 
diff --git a/clang/test/Sema/x86_64-linux-android.c b/clang/test/Sema/x86_64-linux-android.c
index 252570579f607..75158dc066dc1 100644
--- a/clang/test/Sema/x86_64-linux-android.c
+++ b/clang/test/Sema/x86_64-linux-android.c
@@ -7,4 +7,4 @@ extern int a1_i[__alignof(long double) == 16 ? 1 : -1];
 // Verify that long double is 128 bit IEEEquad
 
 long double foo = 1.0E4000L;
-// CHECK: long double foo = 1.00000000000000000000000000000000004E+4000L;
+// CHECK: long double foo = 1.0E+4000L;
diff --git a/llvm/include/llvm/ADT/APFloat.h b/llvm/include/llvm/ADT/APFloat.h
index a9a3dc89a1730..770ad0236ecf5 100644
--- a/llvm/include/llvm/ADT/APFloat.h
+++ b/llvm/include/llvm/ADT/APFloat.h
@@ -1615,6 +1615,20 @@ class APFloat : public APFloatBase {
         toString(Str, FormatPrecision, FormatMaxPadding, TruncateZero));
   }
 
+  /// Format the value as toString does and return true if the string parses
+  /// back bitwise equal. Non-finite spellings ("+Inf", "NaN") parse back
+  /// here but may not fit a caller's grammar.
+  LLVM_ABI bool toStringRoundTrip(SmallVectorImpl<char> &Str,
+                                  unsigned FormatPrecision = 0,
+                                  unsigned FormatMaxPadding = 3,
+                                  bool TruncateZero = true) const;
+
+  /// Like toString, but picks the smallest FormatPrecision whose output
+  /// parses back bitwise equal: "3.14", not "3.1400000000000001".
+  LLVM_ABI void toStringShortest(SmallVectorImpl<char> &Str,
+                                 unsigned FormatMaxPadding = 3,
+                                 bool TruncateZero = true) const;
+
   LLVM_ABI void print(raw_ostream &) const;
 
 #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index c3202eea12c28..fb746cbecb6f3 100644
--- a/llvm/lib/IR/AsmWriter.cpp
+++ b/llvm/lib/IR/AsmWriter.cpp
@@ -1589,8 +1589,8 @@ static void writeAPFloatInternal(raw_ostream &Out, const APFloat &APF) {
   // same APFloat value, then we know that it is safe to use it. Otherwise, fall
   // back onto the hexadecimal format.
   SmallString<128> StrVal;
-  APF.toString(StrVal, 6, 0, false);
-  if (APFloat(APF.getSemantics(), StrVal) == APF) {
+  if (APF.toStringRoundTrip(StrVal, /*FormatPrecision=*/6,
+                            /*FormatMaxPadding=*/0, /*TruncateZero=*/false)) {
     Out << StrVal;
     return;
   }
diff --git a/llvm/lib/Support/APFloat.cpp b/llvm/lib/Support/APFloat.cpp
index 22528d9e93f73..0b7452208fcae 100644
--- a/llvm/lib/Support/APFloat.cpp
+++ b/llvm/lib/Support/APFloat.cpp
@@ -18,6 +18,7 @@
 #include "llvm/ADT/FoldingSet.h"
 #include "llvm/ADT/Hashing.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
@@ -5973,6 +5974,43 @@ APFloat APFloat::getAllOnesValue(const fltSemantics &Semantics) {
   return APFloat(Semantics, APInt::getAllOnes(Semantics.sizeInBits));
 }
 
+bool APFloat::toStringRoundTrip(SmallVectorImpl<char> &Str,
+                                unsigned FormatPrecision,
+                                unsigned FormatMaxPadding,
+                                bool TruncateZero) const {
+  SmallString<32> Buf;
+  toString(Buf, FormatPrecision, FormatMaxPadding, TruncateZero);
+  Str.append(Buf.begin(), Buf.end());
+  APFloat Parsed(getSemantics());
+  Expected<opStatus> Status =
+      Parsed.convertFromString(Buf, rmNearestTiesToEven);
+  if (!Status) {
+    consumeError(Status.takeError());
+    return false;
+  }
+  return Parsed.bitwiseIsEqual(*this);
+}
+
+void APFloat::toStringShortest(SmallVectorImpl<char> &Str,
+                               unsigned FormatMaxPadding,
+                               bool TruncateZero) const {
+  SmallString<32> Best;
+  toString(Best, /*FormatPrecision=*/0, FormatMaxPadding, TruncateZero);
+  if (isFiniteNonZero()) {
+    // Probe below the conservative natural precision (toStringImpl's FIXME).
+    for (unsigned Precision = 1, MaxPrecision = Best.size();
+         Precision < MaxPrecision; ++Precision) {
+      SmallString<32> Candidate;
+      if (toStringRoundTrip(Candidate, Precision, FormatMaxPadding,
+                            TruncateZero)) {
+        Best = Candidate;
+        break;
+      }
+    }
+  }
+  Str.append(Best.begin(), Best.end());
+}
+
 void APFloat::print(raw_ostream &OS) const {
   SmallVector<char, 16> Buffer;
   toString(Buffer);
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 0993441abf22d..81445fd9a777d 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -99,9 +99,9 @@ void AnyValue::print(Context &Ctx, raw_ostream &OS) const {
     // exponential notation if it is lossless, otherwise output it in
     // hexadecimal notation.
     SmallString<16> StrVal;
-    FloatVal.toString(StrVal, /*FormatPrecision=*/6, /*FormatMaxPadding=*/0,
-                      /*TruncateZero=*/false);
-    if (APFloat(FloatVal.getSemantics(), StrVal).bitwiseIsEqual(FloatVal)) {
+    if (FloatVal.toStringRoundTrip(StrVal, /*FormatPrecision=*/6,
+                                   /*FormatMaxPadding=*/0,
+                                   /*TruncateZero=*/false)) {
       OS << StrVal;
     } else {
       StrVal.clear();
diff --git a/llvm/unittests/ADT/APFloatTest.cpp b/llvm/unittests/ADT/APFloatTest.cpp
index ca93953537a09..94072a914da67 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -1629,6 +1629,74 @@ TEST(APFloatTest, toString) {
   }
 }
 
+static std::string convertToStringShortest(const APFloat &F, unsigned Pad = 3,
+                                           bool Tr = true) {
+  llvm::SmallVector<char, 100> Buffer;
+  F.toStringShortest(Buffer, Pad, Tr);
+  return std::string(Buffer.data(), Buffer.size());
+}
+
+TEST(APFloatTest, toStringRoundTrip) {
+  SmallString<32> Str;
+  // 6-digit formatting is lossless for 3.14 but lossy for pi/4; the string
+  // is appended either way (mlir::AsmPrinter's printFloatValue contract).
+  ASSERT_TRUE(APFloat(3.14).toStringRoundTrip(Str, 6, 0, false));
+  ASSERT_EQ("3.140000e+00", Str);
+  Str.clear();
+  ASSERT_FALSE(
+      APFloat(0.78539816339744830961).toStringRoundTrip(Str, 6, 0, false));
+  ASSERT_EQ("7.853980e-01", Str);
+  Str.clear();
+  // FormatPrecision = 0 (natural precision) always round-trips.
+  ASSERT_TRUE(APFloat(0.78539816339744830961).toStringRoundTrip(Str));
+  ASSERT_EQ("0.78539816339744828", Str);
+  Str.clear();
+  // The Inf spelling parses back via convertFromString, although it is not
+  // valid in the IR or MLIR grammars (callers gate non-finite values).
+  ASSERT_TRUE(APFloat::getInf(APFloat::IEEEdouble()).toStringRoundTrip(Str));
+  ASSERT_EQ("+Inf", Str);
+}
+
+TEST(APFloatTest, toStringShortest) {
+  ASSERT_EQ("3.14", convertToStringShortest(APFloat(3.14)));
+  ASSERT_EQ("-3.14", convertToStringShortest(APFloat(-3.14)));
+  ASSERT_EQ("4", convertToStringShortest(APFloat(4.0)));
+  ASSERT_EQ("0.5", convertToStringShortest(APFloat(0.5)));
+  ASSERT_EQ("0.1", convertToStringShortest(APFloat(0.1)));
+  ASSERT_EQ("873.1834", convertToStringShortest(APFloat(873.1834)));
+  ASSERT_EQ("1.0E+10", convertToStringShortest(APFloat(1e10)));
+  // DBL_MAX needs every natural-precision digit.
+  ASSERT_EQ("1.7976931348623157E+308",
+            convertToStringShortest(APFloat(1.7976931348623157E+308)));
+  // The smallest double denormal.
+  ASSERT_EQ("5.0E-324",
+            convertToStringShortest(APFloat(4.9406564584124654e-324)));
+
+  // Non-double semantics.
+  ASSERT_EQ("3.14", convertToStringShortest(APFloat(3.14f)));
+  ASSERT_EQ("0.1", convertToStringShortest(APFloat(0.1f)));
+  ASSERT_EQ("0.1", convertToStringShortest(
+                       APFloat(APFloat::x87DoubleExtended(), "0.1")));
+  ASSERT_EQ("0.1",
+            convertToStringShortest(APFloat(APFloat::IEEEquad(), "0.1")));
+  ASSERT_EQ("0.1", convertToStringShortest(
+                       APFloat(APFloat::PPCDoubleDouble(), "0.1")));
+
+  // FormatMaxPadding and TruncateZero pass through to toString.
+  ASSERT_EQ("1.0E+1", convertToStringShortest(APFloat(10.0), 0));
+  ASSERT_EQ("1.0e+01", convertToStringShortest(APFloat(10.0), 0, false));
+
+  // Zero and non-finite values format exactly as toString.
+  ASSERT_EQ("0", convertToStringShortest(APFloat(0.0)));
+  ASSERT_EQ("-0", convertToStringShortest(APFloat(-0.0)));
+  ASSERT_EQ("+Inf",
+            convertToStringShortest(APFloat::getInf(APFloat::IEEEdouble())));
+  ASSERT_EQ("-Inf", convertToStringShortest(
+                        APFloat::getInf(APFloat::IEEEdouble(), true)));
+  ASSERT_EQ("NaN",
+            convertToStringShortest(APFloat::getNaN(APFloat::IEEEdouble())));
+}
+
 TEST(APFloatTest, toInteger) {
   bool isExact = false;
   APSInt result(5, /*isUnsigned=*/true);
diff --git a/mlir/lib/IR/AsmPrinter.cpp b/mlir/lib/IR/AsmPrinter.cpp
index 47a33a116f92c..274e9976e5733 100644
--- a/mlir/lib/IR/AsmPrinter.cpp
+++ b/mlir/lib/IR/AsmPrinter.cpp
@@ -2265,8 +2265,10 @@ static void printFloatValue(const APFloat &apValue, raw_ostream &os,
   bool isNaN = apValue.isNaN();
   if (!isInf && !isNaN) {
     SmallString<128> strValue;
-    apValue.toString(strValue, /*FormatPrecision=*/6, /*FormatMaxPadding=*/0,
-                     /*TruncateZero=*/false);
+    bool isLossless = apValue.toStringRoundTrip(strValue,
+                                                /*FormatPrecision=*/6,
+                                                /*FormatMaxPadding=*/0,
+                                                /*TruncateZero=*/false);
 
     // Check to make sure that the stringized number is not some string like
     // "Inf" or NaN, that atof will accept, but the lexer will not.  Check
@@ -2276,9 +2278,9 @@ static void printFloatValue(const APFloat &apValue, raw_ostream &os,
              (strValue[1] >= '0' && strValue[1] <= '9'))) &&
            "[-+]?[0-9] regex does not match!");
 
-    // Parse back the stringized version and check that the value is equal
-    // (i.e., there is no precision loss).
-    if (APFloat(apValue.getSemantics(), strValue).bitwiseIsEqual(apValue)) {
+    // The stringized version has no precision loss if it parses back to the
+    // same value.
+    if (isLossless) {
       os << strValue;
       return;
     }

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-llvm-adt

Author: Emery Conrad (conrade-ctc)

Changes

StmtPrinter printed a FloatingLiteral at the maximum round-trip precision, so 3.14 printed as 3.1400000000000001. This patch adds APFloat::toStringShortest, which probes increasing precisions and keeps the first string that parses back bitwise-equal, and calls it from PrintFloatingLiteral. NaN, infinity, and zero keep the old path. Values that need full precision (e.g. DBL_MAX) print as before.

The probe step is a new APFloat::toStringRoundTrip: format at a given precision and report whether the string parses back bitwise-equal. The IR, MLIR, and llubi printers hand-rolled the same check and now call the helper, with no output change there. A future shortest-digits algorithm has a single landing spot.

Three tests pinned the old maximum-precision output and were updated. A new -ast-print test covers double/float/x87/quad, denormals, and a full-precision value; new unit tests cover both helpers.

🤖 Done with the help of Claude Code (Fable 5, human in the loop)


Full diff: https://github.com/llvm/llvm-project/pull/218471.diff

12 Files Affected:

  • (modified) clang/docs/ReleaseNotes.md (+4)
  • (modified) clang/lib/AST/StmtPrinter.cpp (+1-1)
  • (added) clang/test/AST/ast-print-float-shortest.cpp (+41)
  • (modified) clang/test/PCH/floating-literal.c (+2-2)
  • (modified) clang/test/PCH/objc_literals.m (+3-3)
  • (modified) clang/test/Sema/x86_64-linux-android.c (+1-1)
  • (modified) llvm/include/llvm/ADT/APFloat.h (+14)
  • (modified) llvm/lib/IR/AsmWriter.cpp (+2-2)
  • (modified) llvm/lib/Support/APFloat.cpp (+38)
  • (modified) llvm/tools/llubi/lib/Value.cpp (+3-3)
  • (modified) llvm/unittests/ADT/APFloatTest.cpp (+68)
  • (modified) mlir/lib/IR/AsmPrinter.cpp (+7-5)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index ca0dbfa2af229..078d0a74bbf4b 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -98,6 +98,10 @@ features cannot lower the translation-unit ABI level;
 
 ### AST Dumping Potentially Breaking Changes
 
+- The AST printer (`-ast-print`) now prints a floating-point literal as the
+  shortest decimal form that round-trips to the same value. For example,
+  `3.14` printed as `3.1400000000000001` before and prints as `3.14` now.
+
 ### Clang Frontend Potentially Breaking Changes
 
 - Templight support has been removed.
diff --git a/clang/lib/AST/StmtPrinter.cpp b/clang/lib/AST/StmtPrinter.cpp
index e957b03b7e898..30df61440e15f 100644
--- a/clang/lib/AST/StmtPrinter.cpp
+++ b/clang/lib/AST/StmtPrinter.cpp
@@ -1594,7 +1594,7 @@ void StmtPrinter::VisitFixedPointLiteral(FixedPointLiteral *Node) {
 static void PrintFloatingLiteral(raw_ostream &OS, FloatingLiteral *Node,
                                  bool PrintSuffix) {
   SmallString<16> Str;
-  Node->getValue().toString(Str);
+  Node->getValue().toStringShortest(Str);
   OS << Str;
   if (Str.find_first_not_of("-0123456789") == StringRef::npos)
     OS << '.'; // Trailing dot in order to separate from ints.
diff --git a/clang/test/AST/ast-print-float-shortest.cpp b/clang/test/AST/ast-print-float-shortest.cpp
new file mode 100644
index 0000000000000..b5f21a7ae27ec
--- /dev/null
+++ b/clang/test/AST/ast-print-float-shortest.cpp
@@ -0,0 +1,41 @@
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -ast-print %s | FileCheck %s
+
+// Floating literals print as the shortest decimal form that round-trips.
+
+double d1 = 3.14;
+// CHECK: double d1 = 3.14;
+double d2 = 4.0;
+// CHECK: double d2 = 4.;
+double d3 = 0.5;
+// CHECK: double d3 = 0.5;
+double d4 = 1e10;
+// CHECK: double d4 = 1.0E+10;
+double d5 = 1e-300;
+// CHECK: double d5 = 1.0E-300;
+double d6 = 0.1;
+// CHECK: double d6 = 0.1;
+// The smallest double denormal: the shortest round-trip form is 5E-324.
+double d7 = 4.9406564584124654e-324;
+// CHECK: double d7 = 5.0E-324;
+// DBL_MAX needs all 17 significant digits; nothing shorter round-trips.
+double d8 = 1.7976931348623157e+308;
+// CHECK: double d8 = 1.7976931348623157E+308;
+double d9 = -3.14;
+// CHECK: double d9 = -3.14;
+
+float f1 = 3.14f;
+// CHECK: float f1 = 3.14F;
+float f2 = 0.1f;
+// CHECK: float f2 = 0.1F;
+// A hex float keeps its value, printed in decimal.
+float f3 = 0x1.8p3f;
+// CHECK: float f3 = 12.F;
+
+// x86 80-bit extended precision.
+long double ld1 = 0.1L;
+// CHECK: long double ld1 = 0.1L;
+long double ld2 = 3.14L;
+// CHECK: long double ld2 = 3.14L;
+
+__float128 q1 = 0.1q;
+// CHECK: __float128 q1 = 0.1Q;
diff --git a/clang/test/PCH/floating-literal.c b/clang/test/PCH/floating-literal.c
index b5ff6fe84b88c..4590b1990b79d 100644
--- a/clang/test/PCH/floating-literal.c
+++ b/clang/test/PCH/floating-literal.c
@@ -8,12 +8,12 @@
 // targets with 128-bit IEEE long doubles.
 
 long double foo = 1.0E4000L;
-// CHECK: long double foo = 1.00000000000000000000000000000000004E+4000L;
+// CHECK: long double foo = 1.0E+4000L;
 
 // Just as well check the others are still sane while we're here...
 
 double bar = 1.0E300;
-// CHECK: double bar = 1.0000000000000001E+300;
+// CHECK: double bar = 1.0E+300;
 
 float wibble = 1.0E40;
 // CHECK: float wibble = 1.0E+40;
diff --git a/clang/test/PCH/objc_literals.m b/clang/test/PCH/objc_literals.m
index 16ed6aeb9b933..ede3358e2fe69 100644
--- a/clang/test/PCH/objc_literals.m
+++ b/clang/test/PCH/objc_literals.m
@@ -45,18 +45,18 @@ static inline void test_numeric_literals(void) {
   // CHECK-PRINT: id intlit = @17
   // CHECK-IR: {{call.*17}}
   id intlit = @17;
-  // CHECK-PRINT: id floatlit = @17.449999999999999
+  // CHECK-PRINT: id floatlit = @17.45
   // CHECK-IR: {{call.*1.745}}
   id floatlit = @17.45;
 }
 
 static inline void test_array_literals(void) {
-  // CHECK-PRINT: id arraylit = @[ @17, @17.449999999999999
+  // CHECK-PRINT: id arraylit = @[ @17, @17.45
   id arraylit = @[@17, @17.45];
 }
 
 static inline void test_dictionary_literals(void) {
-  // CHECK-PRINT: id dictlit = @{ @17 : {{@17.449999999999999[^,]*}}, @"hello" : @"world" };
+  // CHECK-PRINT: id dictlit = @{ @17 : {{@17.45[^,]*}}, @"hello" : @"world" };
   id dictlit = @{@17 : @17.45, @"hello" : @"world" };
 }
 
diff --git a/clang/test/Sema/x86_64-linux-android.c b/clang/test/Sema/x86_64-linux-android.c
index 252570579f607..75158dc066dc1 100644
--- a/clang/test/Sema/x86_64-linux-android.c
+++ b/clang/test/Sema/x86_64-linux-android.c
@@ -7,4 +7,4 @@ extern int a1_i[__alignof(long double) == 16 ? 1 : -1];
 // Verify that long double is 128 bit IEEEquad
 
 long double foo = 1.0E4000L;
-// CHECK: long double foo = 1.00000000000000000000000000000000004E+4000L;
+// CHECK: long double foo = 1.0E+4000L;
diff --git a/llvm/include/llvm/ADT/APFloat.h b/llvm/include/llvm/ADT/APFloat.h
index a9a3dc89a1730..770ad0236ecf5 100644
--- a/llvm/include/llvm/ADT/APFloat.h
+++ b/llvm/include/llvm/ADT/APFloat.h
@@ -1615,6 +1615,20 @@ class APFloat : public APFloatBase {
         toString(Str, FormatPrecision, FormatMaxPadding, TruncateZero));
   }
 
+  /// Format the value as toString does and return true if the string parses
+  /// back bitwise equal. Non-finite spellings ("+Inf", "NaN") parse back
+  /// here but may not fit a caller's grammar.
+  LLVM_ABI bool toStringRoundTrip(SmallVectorImpl<char> &Str,
+                                  unsigned FormatPrecision = 0,
+                                  unsigned FormatMaxPadding = 3,
+                                  bool TruncateZero = true) const;
+
+  /// Like toString, but picks the smallest FormatPrecision whose output
+  /// parses back bitwise equal: "3.14", not "3.1400000000000001".
+  LLVM_ABI void toStringShortest(SmallVectorImpl<char> &Str,
+                                 unsigned FormatMaxPadding = 3,
+                                 bool TruncateZero = true) const;
+
   LLVM_ABI void print(raw_ostream &) const;
 
 #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index c3202eea12c28..fb746cbecb6f3 100644
--- a/llvm/lib/IR/AsmWriter.cpp
+++ b/llvm/lib/IR/AsmWriter.cpp
@@ -1589,8 +1589,8 @@ static void writeAPFloatInternal(raw_ostream &Out, const APFloat &APF) {
   // same APFloat value, then we know that it is safe to use it. Otherwise, fall
   // back onto the hexadecimal format.
   SmallString<128> StrVal;
-  APF.toString(StrVal, 6, 0, false);
-  if (APFloat(APF.getSemantics(), StrVal) == APF) {
+  if (APF.toStringRoundTrip(StrVal, /*FormatPrecision=*/6,
+                            /*FormatMaxPadding=*/0, /*TruncateZero=*/false)) {
     Out << StrVal;
     return;
   }
diff --git a/llvm/lib/Support/APFloat.cpp b/llvm/lib/Support/APFloat.cpp
index 22528d9e93f73..0b7452208fcae 100644
--- a/llvm/lib/Support/APFloat.cpp
+++ b/llvm/lib/Support/APFloat.cpp
@@ -18,6 +18,7 @@
 #include "llvm/ADT/FoldingSet.h"
 #include "llvm/ADT/Hashing.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
@@ -5973,6 +5974,43 @@ APFloat APFloat::getAllOnesValue(const fltSemantics &Semantics) {
   return APFloat(Semantics, APInt::getAllOnes(Semantics.sizeInBits));
 }
 
+bool APFloat::toStringRoundTrip(SmallVectorImpl<char> &Str,
+                                unsigned FormatPrecision,
+                                unsigned FormatMaxPadding,
+                                bool TruncateZero) const {
+  SmallString<32> Buf;
+  toString(Buf, FormatPrecision, FormatMaxPadding, TruncateZero);
+  Str.append(Buf.begin(), Buf.end());
+  APFloat Parsed(getSemantics());
+  Expected<opStatus> Status =
+      Parsed.convertFromString(Buf, rmNearestTiesToEven);
+  if (!Status) {
+    consumeError(Status.takeError());
+    return false;
+  }
+  return Parsed.bitwiseIsEqual(*this);
+}
+
+void APFloat::toStringShortest(SmallVectorImpl<char> &Str,
+                               unsigned FormatMaxPadding,
+                               bool TruncateZero) const {
+  SmallString<32> Best;
+  toString(Best, /*FormatPrecision=*/0, FormatMaxPadding, TruncateZero);
+  if (isFiniteNonZero()) {
+    // Probe below the conservative natural precision (toStringImpl's FIXME).
+    for (unsigned Precision = 1, MaxPrecision = Best.size();
+         Precision < MaxPrecision; ++Precision) {
+      SmallString<32> Candidate;
+      if (toStringRoundTrip(Candidate, Precision, FormatMaxPadding,
+                            TruncateZero)) {
+        Best = Candidate;
+        break;
+      }
+    }
+  }
+  Str.append(Best.begin(), Best.end());
+}
+
 void APFloat::print(raw_ostream &OS) const {
   SmallVector<char, 16> Buffer;
   toString(Buffer);
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 0993441abf22d..81445fd9a777d 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -99,9 +99,9 @@ void AnyValue::print(Context &Ctx, raw_ostream &OS) const {
     // exponential notation if it is lossless, otherwise output it in
     // hexadecimal notation.
     SmallString<16> StrVal;
-    FloatVal.toString(StrVal, /*FormatPrecision=*/6, /*FormatMaxPadding=*/0,
-                      /*TruncateZero=*/false);
-    if (APFloat(FloatVal.getSemantics(), StrVal).bitwiseIsEqual(FloatVal)) {
+    if (FloatVal.toStringRoundTrip(StrVal, /*FormatPrecision=*/6,
+                                   /*FormatMaxPadding=*/0,
+                                   /*TruncateZero=*/false)) {
       OS << StrVal;
     } else {
       StrVal.clear();
diff --git a/llvm/unittests/ADT/APFloatTest.cpp b/llvm/unittests/ADT/APFloatTest.cpp
index ca93953537a09..94072a914da67 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -1629,6 +1629,74 @@ TEST(APFloatTest, toString) {
   }
 }
 
+static std::string convertToStringShortest(const APFloat &F, unsigned Pad = 3,
+                                           bool Tr = true) {
+  llvm::SmallVector<char, 100> Buffer;
+  F.toStringShortest(Buffer, Pad, Tr);
+  return std::string(Buffer.data(), Buffer.size());
+}
+
+TEST(APFloatTest, toStringRoundTrip) {
+  SmallString<32> Str;
+  // 6-digit formatting is lossless for 3.14 but lossy for pi/4; the string
+  // is appended either way (mlir::AsmPrinter's printFloatValue contract).
+  ASSERT_TRUE(APFloat(3.14).toStringRoundTrip(Str, 6, 0, false));
+  ASSERT_EQ("3.140000e+00", Str);
+  Str.clear();
+  ASSERT_FALSE(
+      APFloat(0.78539816339744830961).toStringRoundTrip(Str, 6, 0, false));
+  ASSERT_EQ("7.853980e-01", Str);
+  Str.clear();
+  // FormatPrecision = 0 (natural precision) always round-trips.
+  ASSERT_TRUE(APFloat(0.78539816339744830961).toStringRoundTrip(Str));
+  ASSERT_EQ("0.78539816339744828", Str);
+  Str.clear();
+  // The Inf spelling parses back via convertFromString, although it is not
+  // valid in the IR or MLIR grammars (callers gate non-finite values).
+  ASSERT_TRUE(APFloat::getInf(APFloat::IEEEdouble()).toStringRoundTrip(Str));
+  ASSERT_EQ("+Inf", Str);
+}
+
+TEST(APFloatTest, toStringShortest) {
+  ASSERT_EQ("3.14", convertToStringShortest(APFloat(3.14)));
+  ASSERT_EQ("-3.14", convertToStringShortest(APFloat(-3.14)));
+  ASSERT_EQ("4", convertToStringShortest(APFloat(4.0)));
+  ASSERT_EQ("0.5", convertToStringShortest(APFloat(0.5)));
+  ASSERT_EQ("0.1", convertToStringShortest(APFloat(0.1)));
+  ASSERT_EQ("873.1834", convertToStringShortest(APFloat(873.1834)));
+  ASSERT_EQ("1.0E+10", convertToStringShortest(APFloat(1e10)));
+  // DBL_MAX needs every natural-precision digit.
+  ASSERT_EQ("1.7976931348623157E+308",
+            convertToStringShortest(APFloat(1.7976931348623157E+308)));
+  // The smallest double denormal.
+  ASSERT_EQ("5.0E-324",
+            convertToStringShortest(APFloat(4.9406564584124654e-324)));
+
+  // Non-double semantics.
+  ASSERT_EQ("3.14", convertToStringShortest(APFloat(3.14f)));
+  ASSERT_EQ("0.1", convertToStringShortest(APFloat(0.1f)));
+  ASSERT_EQ("0.1", convertToStringShortest(
+                       APFloat(APFloat::x87DoubleExtended(), "0.1")));
+  ASSERT_EQ("0.1",
+            convertToStringShortest(APFloat(APFloat::IEEEquad(), "0.1")));
+  ASSERT_EQ("0.1", convertToStringShortest(
+                       APFloat(APFloat::PPCDoubleDouble(), "0.1")));
+
+  // FormatMaxPadding and TruncateZero pass through to toString.
+  ASSERT_EQ("1.0E+1", convertToStringShortest(APFloat(10.0), 0));
+  ASSERT_EQ("1.0e+01", convertToStringShortest(APFloat(10.0), 0, false));
+
+  // Zero and non-finite values format exactly as toString.
+  ASSERT_EQ("0", convertToStringShortest(APFloat(0.0)));
+  ASSERT_EQ("-0", convertToStringShortest(APFloat(-0.0)));
+  ASSERT_EQ("+Inf",
+            convertToStringShortest(APFloat::getInf(APFloat::IEEEdouble())));
+  ASSERT_EQ("-Inf", convertToStringShortest(
+                        APFloat::getInf(APFloat::IEEEdouble(), true)));
+  ASSERT_EQ("NaN",
+            convertToStringShortest(APFloat::getNaN(APFloat::IEEEdouble())));
+}
+
 TEST(APFloatTest, toInteger) {
   bool isExact = false;
   APSInt result(5, /*isUnsigned=*/true);
diff --git a/mlir/lib/IR/AsmPrinter.cpp b/mlir/lib/IR/AsmPrinter.cpp
index 47a33a116f92c..274e9976e5733 100644
--- a/mlir/lib/IR/AsmPrinter.cpp
+++ b/mlir/lib/IR/AsmPrinter.cpp
@@ -2265,8 +2265,10 @@ static void printFloatValue(const APFloat &apValue, raw_ostream &os,
   bool isNaN = apValue.isNaN();
   if (!isInf && !isNaN) {
     SmallString<128> strValue;
-    apValue.toString(strValue, /*FormatPrecision=*/6, /*FormatMaxPadding=*/0,
-                     /*TruncateZero=*/false);
+    bool isLossless = apValue.toStringRoundTrip(strValue,
+                                                /*FormatPrecision=*/6,
+                                                /*FormatMaxPadding=*/0,
+                                                /*TruncateZero=*/false);
 
     // Check to make sure that the stringized number is not some string like
     // "Inf" or NaN, that atof will accept, but the lexer will not.  Check
@@ -2276,9 +2278,9 @@ static void printFloatValue(const APFloat &apValue, raw_ostream &os,
              (strValue[1] >= '0' && strValue[1] <= '9'))) &&
            "[-+]?[0-9] regex does not match!");
 
-    // Parse back the stringized version and check that the value is equal
-    // (i.e., there is no precision loss).
-    if (APFloat(apValue.getSemantics(), strValue).bitwiseIsEqual(apValue)) {
+    // The stringized version has no precision loss if it parses back to the
+    // same value.
+    if (isLossless) {
       os << strValue;
       return;
     }

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-llvm-ir

Author: Emery Conrad (conrade-ctc)

Changes

StmtPrinter printed a FloatingLiteral at the maximum round-trip precision, so 3.14 printed as 3.1400000000000001. This patch adds APFloat::toStringShortest, which probes increasing precisions and keeps the first string that parses back bitwise-equal, and calls it from PrintFloatingLiteral. NaN, infinity, and zero keep the old path. Values that need full precision (e.g. DBL_MAX) print as before.

The probe step is a new APFloat::toStringRoundTrip: format at a given precision and report whether the string parses back bitwise-equal. The IR, MLIR, and llubi printers hand-rolled the same check and now call the helper, with no output change there. A future shortest-digits algorithm has a single landing spot.

Three tests pinned the old maximum-precision output and were updated. A new -ast-print test covers double/float/x87/quad, denormals, and a full-precision value; new unit tests cover both helpers.

🤖 Done with the help of Claude Code (Fable 5, human in the loop)


Full diff: https://github.com/llvm/llvm-project/pull/218471.diff

12 Files Affected:

  • (modified) clang/docs/ReleaseNotes.md (+4)
  • (modified) clang/lib/AST/StmtPrinter.cpp (+1-1)
  • (added) clang/test/AST/ast-print-float-shortest.cpp (+41)
  • (modified) clang/test/PCH/floating-literal.c (+2-2)
  • (modified) clang/test/PCH/objc_literals.m (+3-3)
  • (modified) clang/test/Sema/x86_64-linux-android.c (+1-1)
  • (modified) llvm/include/llvm/ADT/APFloat.h (+14)
  • (modified) llvm/lib/IR/AsmWriter.cpp (+2-2)
  • (modified) llvm/lib/Support/APFloat.cpp (+38)
  • (modified) llvm/tools/llubi/lib/Value.cpp (+3-3)
  • (modified) llvm/unittests/ADT/APFloatTest.cpp (+68)
  • (modified) mlir/lib/IR/AsmPrinter.cpp (+7-5)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index ca0dbfa2af229..078d0a74bbf4b 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -98,6 +98,10 @@ features cannot lower the translation-unit ABI level;
 
 ### AST Dumping Potentially Breaking Changes
 
+- The AST printer (`-ast-print`) now prints a floating-point literal as the
+  shortest decimal form that round-trips to the same value. For example,
+  `3.14` printed as `3.1400000000000001` before and prints as `3.14` now.
+
 ### Clang Frontend Potentially Breaking Changes
 
 - Templight support has been removed.
diff --git a/clang/lib/AST/StmtPrinter.cpp b/clang/lib/AST/StmtPrinter.cpp
index e957b03b7e898..30df61440e15f 100644
--- a/clang/lib/AST/StmtPrinter.cpp
+++ b/clang/lib/AST/StmtPrinter.cpp
@@ -1594,7 +1594,7 @@ void StmtPrinter::VisitFixedPointLiteral(FixedPointLiteral *Node) {
 static void PrintFloatingLiteral(raw_ostream &OS, FloatingLiteral *Node,
                                  bool PrintSuffix) {
   SmallString<16> Str;
-  Node->getValue().toString(Str);
+  Node->getValue().toStringShortest(Str);
   OS << Str;
   if (Str.find_first_not_of("-0123456789") == StringRef::npos)
     OS << '.'; // Trailing dot in order to separate from ints.
diff --git a/clang/test/AST/ast-print-float-shortest.cpp b/clang/test/AST/ast-print-float-shortest.cpp
new file mode 100644
index 0000000000000..b5f21a7ae27ec
--- /dev/null
+++ b/clang/test/AST/ast-print-float-shortest.cpp
@@ -0,0 +1,41 @@
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -ast-print %s | FileCheck %s
+
+// Floating literals print as the shortest decimal form that round-trips.
+
+double d1 = 3.14;
+// CHECK: double d1 = 3.14;
+double d2 = 4.0;
+// CHECK: double d2 = 4.;
+double d3 = 0.5;
+// CHECK: double d3 = 0.5;
+double d4 = 1e10;
+// CHECK: double d4 = 1.0E+10;
+double d5 = 1e-300;
+// CHECK: double d5 = 1.0E-300;
+double d6 = 0.1;
+// CHECK: double d6 = 0.1;
+// The smallest double denormal: the shortest round-trip form is 5E-324.
+double d7 = 4.9406564584124654e-324;
+// CHECK: double d7 = 5.0E-324;
+// DBL_MAX needs all 17 significant digits; nothing shorter round-trips.
+double d8 = 1.7976931348623157e+308;
+// CHECK: double d8 = 1.7976931348623157E+308;
+double d9 = -3.14;
+// CHECK: double d9 = -3.14;
+
+float f1 = 3.14f;
+// CHECK: float f1 = 3.14F;
+float f2 = 0.1f;
+// CHECK: float f2 = 0.1F;
+// A hex float keeps its value, printed in decimal.
+float f3 = 0x1.8p3f;
+// CHECK: float f3 = 12.F;
+
+// x86 80-bit extended precision.
+long double ld1 = 0.1L;
+// CHECK: long double ld1 = 0.1L;
+long double ld2 = 3.14L;
+// CHECK: long double ld2 = 3.14L;
+
+__float128 q1 = 0.1q;
+// CHECK: __float128 q1 = 0.1Q;
diff --git a/clang/test/PCH/floating-literal.c b/clang/test/PCH/floating-literal.c
index b5ff6fe84b88c..4590b1990b79d 100644
--- a/clang/test/PCH/floating-literal.c
+++ b/clang/test/PCH/floating-literal.c
@@ -8,12 +8,12 @@
 // targets with 128-bit IEEE long doubles.
 
 long double foo = 1.0E4000L;
-// CHECK: long double foo = 1.00000000000000000000000000000000004E+4000L;
+// CHECK: long double foo = 1.0E+4000L;
 
 // Just as well check the others are still sane while we're here...
 
 double bar = 1.0E300;
-// CHECK: double bar = 1.0000000000000001E+300;
+// CHECK: double bar = 1.0E+300;
 
 float wibble = 1.0E40;
 // CHECK: float wibble = 1.0E+40;
diff --git a/clang/test/PCH/objc_literals.m b/clang/test/PCH/objc_literals.m
index 16ed6aeb9b933..ede3358e2fe69 100644
--- a/clang/test/PCH/objc_literals.m
+++ b/clang/test/PCH/objc_literals.m
@@ -45,18 +45,18 @@ static inline void test_numeric_literals(void) {
   // CHECK-PRINT: id intlit = @17
   // CHECK-IR: {{call.*17}}
   id intlit = @17;
-  // CHECK-PRINT: id floatlit = @17.449999999999999
+  // CHECK-PRINT: id floatlit = @17.45
   // CHECK-IR: {{call.*1.745}}
   id floatlit = @17.45;
 }
 
 static inline void test_array_literals(void) {
-  // CHECK-PRINT: id arraylit = @[ @17, @17.449999999999999
+  // CHECK-PRINT: id arraylit = @[ @17, @17.45
   id arraylit = @[@17, @17.45];
 }
 
 static inline void test_dictionary_literals(void) {
-  // CHECK-PRINT: id dictlit = @{ @17 : {{@17.449999999999999[^,]*}}, @"hello" : @"world" };
+  // CHECK-PRINT: id dictlit = @{ @17 : {{@17.45[^,]*}}, @"hello" : @"world" };
   id dictlit = @{@17 : @17.45, @"hello" : @"world" };
 }
 
diff --git a/clang/test/Sema/x86_64-linux-android.c b/clang/test/Sema/x86_64-linux-android.c
index 252570579f607..75158dc066dc1 100644
--- a/clang/test/Sema/x86_64-linux-android.c
+++ b/clang/test/Sema/x86_64-linux-android.c
@@ -7,4 +7,4 @@ extern int a1_i[__alignof(long double) == 16 ? 1 : -1];
 // Verify that long double is 128 bit IEEEquad
 
 long double foo = 1.0E4000L;
-// CHECK: long double foo = 1.00000000000000000000000000000000004E+4000L;
+// CHECK: long double foo = 1.0E+4000L;
diff --git a/llvm/include/llvm/ADT/APFloat.h b/llvm/include/llvm/ADT/APFloat.h
index a9a3dc89a1730..770ad0236ecf5 100644
--- a/llvm/include/llvm/ADT/APFloat.h
+++ b/llvm/include/llvm/ADT/APFloat.h
@@ -1615,6 +1615,20 @@ class APFloat : public APFloatBase {
         toString(Str, FormatPrecision, FormatMaxPadding, TruncateZero));
   }
 
+  /// Format the value as toString does and return true if the string parses
+  /// back bitwise equal. Non-finite spellings ("+Inf", "NaN") parse back
+  /// here but may not fit a caller's grammar.
+  LLVM_ABI bool toStringRoundTrip(SmallVectorImpl<char> &Str,
+                                  unsigned FormatPrecision = 0,
+                                  unsigned FormatMaxPadding = 3,
+                                  bool TruncateZero = true) const;
+
+  /// Like toString, but picks the smallest FormatPrecision whose output
+  /// parses back bitwise equal: "3.14", not "3.1400000000000001".
+  LLVM_ABI void toStringShortest(SmallVectorImpl<char> &Str,
+                                 unsigned FormatMaxPadding = 3,
+                                 bool TruncateZero = true) const;
+
   LLVM_ABI void print(raw_ostream &) const;
 
 #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index c3202eea12c28..fb746cbecb6f3 100644
--- a/llvm/lib/IR/AsmWriter.cpp
+++ b/llvm/lib/IR/AsmWriter.cpp
@@ -1589,8 +1589,8 @@ static void writeAPFloatInternal(raw_ostream &Out, const APFloat &APF) {
   // same APFloat value, then we know that it is safe to use it. Otherwise, fall
   // back onto the hexadecimal format.
   SmallString<128> StrVal;
-  APF.toString(StrVal, 6, 0, false);
-  if (APFloat(APF.getSemantics(), StrVal) == APF) {
+  if (APF.toStringRoundTrip(StrVal, /*FormatPrecision=*/6,
+                            /*FormatMaxPadding=*/0, /*TruncateZero=*/false)) {
     Out << StrVal;
     return;
   }
diff --git a/llvm/lib/Support/APFloat.cpp b/llvm/lib/Support/APFloat.cpp
index 22528d9e93f73..0b7452208fcae 100644
--- a/llvm/lib/Support/APFloat.cpp
+++ b/llvm/lib/Support/APFloat.cpp
@@ -18,6 +18,7 @@
 #include "llvm/ADT/FoldingSet.h"
 #include "llvm/ADT/Hashing.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
@@ -5973,6 +5974,43 @@ APFloat APFloat::getAllOnesValue(const fltSemantics &Semantics) {
   return APFloat(Semantics, APInt::getAllOnes(Semantics.sizeInBits));
 }
 
+bool APFloat::toStringRoundTrip(SmallVectorImpl<char> &Str,
+                                unsigned FormatPrecision,
+                                unsigned FormatMaxPadding,
+                                bool TruncateZero) const {
+  SmallString<32> Buf;
+  toString(Buf, FormatPrecision, FormatMaxPadding, TruncateZero);
+  Str.append(Buf.begin(), Buf.end());
+  APFloat Parsed(getSemantics());
+  Expected<opStatus> Status =
+      Parsed.convertFromString(Buf, rmNearestTiesToEven);
+  if (!Status) {
+    consumeError(Status.takeError());
+    return false;
+  }
+  return Parsed.bitwiseIsEqual(*this);
+}
+
+void APFloat::toStringShortest(SmallVectorImpl<char> &Str,
+                               unsigned FormatMaxPadding,
+                               bool TruncateZero) const {
+  SmallString<32> Best;
+  toString(Best, /*FormatPrecision=*/0, FormatMaxPadding, TruncateZero);
+  if (isFiniteNonZero()) {
+    // Probe below the conservative natural precision (toStringImpl's FIXME).
+    for (unsigned Precision = 1, MaxPrecision = Best.size();
+         Precision < MaxPrecision; ++Precision) {
+      SmallString<32> Candidate;
+      if (toStringRoundTrip(Candidate, Precision, FormatMaxPadding,
+                            TruncateZero)) {
+        Best = Candidate;
+        break;
+      }
+    }
+  }
+  Str.append(Best.begin(), Best.end());
+}
+
 void APFloat::print(raw_ostream &OS) const {
   SmallVector<char, 16> Buffer;
   toString(Buffer);
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 0993441abf22d..81445fd9a777d 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -99,9 +99,9 @@ void AnyValue::print(Context &Ctx, raw_ostream &OS) const {
     // exponential notation if it is lossless, otherwise output it in
     // hexadecimal notation.
     SmallString<16> StrVal;
-    FloatVal.toString(StrVal, /*FormatPrecision=*/6, /*FormatMaxPadding=*/0,
-                      /*TruncateZero=*/false);
-    if (APFloat(FloatVal.getSemantics(), StrVal).bitwiseIsEqual(FloatVal)) {
+    if (FloatVal.toStringRoundTrip(StrVal, /*FormatPrecision=*/6,
+                                   /*FormatMaxPadding=*/0,
+                                   /*TruncateZero=*/false)) {
       OS << StrVal;
     } else {
       StrVal.clear();
diff --git a/llvm/unittests/ADT/APFloatTest.cpp b/llvm/unittests/ADT/APFloatTest.cpp
index ca93953537a09..94072a914da67 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -1629,6 +1629,74 @@ TEST(APFloatTest, toString) {
   }
 }
 
+static std::string convertToStringShortest(const APFloat &F, unsigned Pad = 3,
+                                           bool Tr = true) {
+  llvm::SmallVector<char, 100> Buffer;
+  F.toStringShortest(Buffer, Pad, Tr);
+  return std::string(Buffer.data(), Buffer.size());
+}
+
+TEST(APFloatTest, toStringRoundTrip) {
+  SmallString<32> Str;
+  // 6-digit formatting is lossless for 3.14 but lossy for pi/4; the string
+  // is appended either way (mlir::AsmPrinter's printFloatValue contract).
+  ASSERT_TRUE(APFloat(3.14).toStringRoundTrip(Str, 6, 0, false));
+  ASSERT_EQ("3.140000e+00", Str);
+  Str.clear();
+  ASSERT_FALSE(
+      APFloat(0.78539816339744830961).toStringRoundTrip(Str, 6, 0, false));
+  ASSERT_EQ("7.853980e-01", Str);
+  Str.clear();
+  // FormatPrecision = 0 (natural precision) always round-trips.
+  ASSERT_TRUE(APFloat(0.78539816339744830961).toStringRoundTrip(Str));
+  ASSERT_EQ("0.78539816339744828", Str);
+  Str.clear();
+  // The Inf spelling parses back via convertFromString, although it is not
+  // valid in the IR or MLIR grammars (callers gate non-finite values).
+  ASSERT_TRUE(APFloat::getInf(APFloat::IEEEdouble()).toStringRoundTrip(Str));
+  ASSERT_EQ("+Inf", Str);
+}
+
+TEST(APFloatTest, toStringShortest) {
+  ASSERT_EQ("3.14", convertToStringShortest(APFloat(3.14)));
+  ASSERT_EQ("-3.14", convertToStringShortest(APFloat(-3.14)));
+  ASSERT_EQ("4", convertToStringShortest(APFloat(4.0)));
+  ASSERT_EQ("0.5", convertToStringShortest(APFloat(0.5)));
+  ASSERT_EQ("0.1", convertToStringShortest(APFloat(0.1)));
+  ASSERT_EQ("873.1834", convertToStringShortest(APFloat(873.1834)));
+  ASSERT_EQ("1.0E+10", convertToStringShortest(APFloat(1e10)));
+  // DBL_MAX needs every natural-precision digit.
+  ASSERT_EQ("1.7976931348623157E+308",
+            convertToStringShortest(APFloat(1.7976931348623157E+308)));
+  // The smallest double denormal.
+  ASSERT_EQ("5.0E-324",
+            convertToStringShortest(APFloat(4.9406564584124654e-324)));
+
+  // Non-double semantics.
+  ASSERT_EQ("3.14", convertToStringShortest(APFloat(3.14f)));
+  ASSERT_EQ("0.1", convertToStringShortest(APFloat(0.1f)));
+  ASSERT_EQ("0.1", convertToStringShortest(
+                       APFloat(APFloat::x87DoubleExtended(), "0.1")));
+  ASSERT_EQ("0.1",
+            convertToStringShortest(APFloat(APFloat::IEEEquad(), "0.1")));
+  ASSERT_EQ("0.1", convertToStringShortest(
+                       APFloat(APFloat::PPCDoubleDouble(), "0.1")));
+
+  // FormatMaxPadding and TruncateZero pass through to toString.
+  ASSERT_EQ("1.0E+1", convertToStringShortest(APFloat(10.0), 0));
+  ASSERT_EQ("1.0e+01", convertToStringShortest(APFloat(10.0), 0, false));
+
+  // Zero and non-finite values format exactly as toString.
+  ASSERT_EQ("0", convertToStringShortest(APFloat(0.0)));
+  ASSERT_EQ("-0", convertToStringShortest(APFloat(-0.0)));
+  ASSERT_EQ("+Inf",
+            convertToStringShortest(APFloat::getInf(APFloat::IEEEdouble())));
+  ASSERT_EQ("-Inf", convertToStringShortest(
+                        APFloat::getInf(APFloat::IEEEdouble(), true)));
+  ASSERT_EQ("NaN",
+            convertToStringShortest(APFloat::getNaN(APFloat::IEEEdouble())));
+}
+
 TEST(APFloatTest, toInteger) {
   bool isExact = false;
   APSInt result(5, /*isUnsigned=*/true);
diff --git a/mlir/lib/IR/AsmPrinter.cpp b/mlir/lib/IR/AsmPrinter.cpp
index 47a33a116f92c..274e9976e5733 100644
--- a/mlir/lib/IR/AsmPrinter.cpp
+++ b/mlir/lib/IR/AsmPrinter.cpp
@@ -2265,8 +2265,10 @@ static void printFloatValue(const APFloat &apValue, raw_ostream &os,
   bool isNaN = apValue.isNaN();
   if (!isInf && !isNaN) {
     SmallString<128> strValue;
-    apValue.toString(strValue, /*FormatPrecision=*/6, /*FormatMaxPadding=*/0,
-                     /*TruncateZero=*/false);
+    bool isLossless = apValue.toStringRoundTrip(strValue,
+                                                /*FormatPrecision=*/6,
+                                                /*FormatMaxPadding=*/0,
+                                                /*TruncateZero=*/false);
 
     // Check to make sure that the stringized number is not some string like
     // "Inf" or NaN, that atof will accept, but the lexer will not.  Check
@@ -2276,9 +2278,9 @@ static void printFloatValue(const APFloat &apValue, raw_ostream &os,
              (strValue[1] >= '0' && strValue[1] <= '9'))) &&
            "[-+]?[0-9] regex does not match!");
 
-    // Parse back the stringized version and check that the value is equal
-    // (i.e., there is no precision loss).
-    if (APFloat(apValue.getSemantics(), strValue).bitwiseIsEqual(apValue)) {
+    // The stringized version has no precision loss if it parses back to the
+    // same value.
+    if (isLossless) {
       os << strValue;
       return;
     }

@conrade-ctc

Copy link
Copy Markdown
Contributor Author

@vgvassilev for viz, this is the PR in response to compiler-research/CppInterOp#1069 (comment).

@vgvassilev

Copy link
Copy Markdown
Contributor

I think this is a really useful improvement for clang's ast printer.

cc: @AaronBallman

conrade-ctc pushed a commit to conrade-ctc/CppInterOp that referenced this pull request Aug 24, 2026
Fails on the first LLVM that prints shortest round-trip floats
(llvm/llvm-project#218471), which changes the output for defaults the
constants-as-written policy cannot cover.

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
conrade-ctc pushed a commit to conrade-ctc/CppInterOp that referenced this pull request Aug 24, 2026
Fails on the first LLVM that prints shortest round-trip floats
(llvm/llvm-project#218471), which changes the output for defaults the
constants-as-written policy cannot cover.

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
SmallString<32> Best;
toString(Best, /*FormatPrecision=*/0, FormatMaxPadding, TruncateZero);
if (isFiniteNonZero()) {
// Probe below the conservative natural precision (toStringImpl's FIXME).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a terribly inefficient way to do this; see https://dl.acm.org/doi/10.1145/3192366.3192369. But I guess the speed doesn't matter that much for ast-print.

@conrade-ctc conrade-ctc Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at paper now, maybe worth updating since it's in a core util that could be re-used :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree that this is a terribly inefficient implementation; the best algorithm right now I think is Dragonbox: https://github.com/jk-jeon/dragonbox/blob/master/other_files/Dragonbox.pdf

(see also https://onlinelibrary.wiley.com/doi/epdf/10.1002/spe.70056 for empirical testing).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jcranmer-intel, nice, that's a more recent ref and mentions ryu, so i'll look at that first and implement accordingly. thanks for pointing this out.

@conrade-ctc conrade-ctc Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@efriedma-quic, @jcranmer-intel, updated and now in the PR. The shortest digit count comes from the value's rounding interval (Ryu's formulation) in exact APInt arithmetic, so every IEEEFloat semantics works; the existing toString formats at that count and the round trip is still verified. For the search step I measured both shapes: a Ryu-style binary search over power-of-ten grids runs 38µs/value on random doubles, the Dragonbox anchor 26µs (old probing loop: 160µs; short literals 8.9 → 5.9µs), so the anchor is what's in the patch. Output is unchanged — exhaustive over all small formats plus millions of random single/double patterns, ~22.5M comparisons. Per-format power tables would be faster still but give up the semantics-generic exact arithmetic, and toString remains the only formatter so output cannot drift. The earlier CI red was the AsmWriter migration swapping numeric equality for bitwise on ppc_fp128 (a -0.0 low component); AsmWriter keeps its numeric comparison now.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 211840 tests passed
  • 6965 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 142034 tests passed
  • 5076 tests skipped

✅ The build succeeded and all tests passed.

conrade-ctc pushed a commit to conrade-ctc/CppInterOp that referenced this pull request Aug 24, 2026
Fails on the first LLVM that prints shortest round-trip floats
(llvm/llvm-project#218471), which changes the output for defaults the
constants-as-written policy cannot cover.

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
@conrade-ctc
conrade-ctc force-pushed the apfloat-tostring-shortest branch from deb6c3d to 50152d5 Compare August 24, 2026 21:42
…trip form

StmtPrinter printed a FloatingLiteral at the maximum round-trip
precision, so 3.14 printed as 3.1400000000000001. Add
APFloat::toStringShortest and call it from PrintFloatingLiteral.
It computes the shortest digit count from the value's rounding interval
(Ryu's formulation with a Dragonbox-style grid anchor) in exact APInt
arithmetic, so every IEEEFloat semantics works, then formats with the
existing toString at that count and verifies the round trip.
PPCDoubleDouble and the largest value of saturating formats keep a
plain probe from precision 1.

The verify step is a new APFloat::toStringRoundTrip; the MLIR and llubi
printers hand-rolled the same bitwise check and now call it, with no
output change. The IR AsmWriter keeps its numeric comparison: a
ppc_fp128 constant whose low double is -0.0 (any negated literal)
parses back with +0.0, and such numerically equal forms print in
decimal. Updated three tests that pinned the old maximum-precision
output; added an -ast-print test and unit tests for both helpers.

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
@conrade-ctc
conrade-ctc force-pushed the apfloat-tostring-shortest branch from 50152d5 to 2aa0b89 Compare August 25, 2026 15:45
conrade-ctc pushed a commit to conrade-ctc/CppInterOp that referenced this pull request Aug 27, 2026
Fails on the first LLVM that prints shortest round-trip floats
(llvm/llvm-project#218471), which changes the output for defaults the
constants-as-written policy cannot cover.

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
conrade-ctc pushed a commit to chicagotrading/CppInterOp that referenced this pull request Aug 27, 2026
Print the default-argument expression with printPretty, the
interpreter's ASTContext, and ConstantsAsWritten = true. The policy
prints literal leaves from their source text, so defaults keep their
spelling (4.0 stays "4.0"). This replaces the AST re-print at maximum
float precision, which turned 3.14 into 3.1400000000000001.

A canary test fails on the first LLVM that prints shortest round-trip
floats (llvm/llvm-project#218471), which changes the output for defaults
the policy cannot cover.

Content matches compiler-research#1069.

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" llvm:adt llvm:ir llvm:support mlir:core MLIR Core Infrastructure mlir tools:llubi

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants