[flang-commits] [flang] [flang][runtime] Implement EX editing for input & output (PR #67208)

Peter Klausler via flang-commits flang-commits at lists.llvm.org
Fri Sep 22 16:38:30 PDT 2023


https://github.com/klausler created https://github.com/llvm/llvm-project/pull/67208

Support the EX edit descriptor for hexadecimal real formatted output and hexadecimal real input for all forms of formatted input.. (We're possibly the first Fortran compiler to support this feature for input editing; only one other can handle EX output editing.)

As true (not BOZ) hexadecimal floating-point constants are not supported in Fortran source code, only in formatted input, the implementation takes place in the I/O editing portion of the runtime, not as new conversions in the Decimal library.

>From 694cd95276e848c266f7ff38b816a74d8c306c03 Mon Sep 17 00:00:00 2001
From: Peter Klausler <pklausler at nvidia.com>
Date: Wed, 20 Sep 2023 16:11:27 -0700
Subject: [PATCH] [flang][runtime] Implement EX editing for input & output

Support the EX edit descriptor for hexadecimal real formatted output
and hexadecimal real input for all forms of formatted input..
(We're possibly the first Fortran compiler to support this feature
for input editing; only one other can handle EX output editing.)

As true (not BOZ) hexadecimal floating-point constants are not
supported in Fortran source code, only in formatted input, the
implementation takes place in the I/O editing portion of the
runtime, not as new conversions in the Decimal library.
---
 flang/include/flang/Common/real.h             |   7 +
 .../flang/Decimal/binary-floating-point.h     |  59 ++++
 flang/include/flang/Decimal/decimal.h         |   8 -
 flang/runtime/edit-input.cpp                  | 293 +++++++++++++-----
 flang/runtime/edit-output.cpp                 | 164 ++++++++--
 flang/runtime/edit-output.h                   |  24 +-
 .../unittests/Runtime/NumericalFormatTest.cpp |  62 +++-
 7 files changed, 495 insertions(+), 122 deletions(-)

diff --git a/flang/include/flang/Common/real.h b/flang/include/flang/Common/real.h
index 036f665d3da6194..50aab7d89a597ef 100644
--- a/flang/include/flang/Common/real.h
+++ b/flang/include/flang/Common/real.h
@@ -63,6 +63,10 @@ static constexpr int MaxDecimalConversionDigits(int binaryPrecision) {
   }
 }
 
+static constexpr int MaxHexadecimalConversionDigits(int binaryPrecision) {
+  return binaryPrecision >= 0 ? (binaryPrecision + 3) / 4 : binaryPrecision;
+}
+
 static constexpr int RealKindForPrecision(int binaryPrecision) {
   switch (binaryPrecision) {
   case 8: // IEEE single (truncated): 1+8+7 with implicit bit
@@ -132,6 +136,9 @@ template <int BINARY_PRECISION> class RealDetails {
   static constexpr int maxDecimalConversionDigits{
       MaxDecimalConversionDigits(binaryPrecision)};
 
+  static constexpr int maxHexadecimalConversionDigits{
+      MaxHexadecimalConversionDigits(binaryPrecision)};
+
   static_assert(binaryPrecision > 0);
   static_assert(exponentBits > 1);
   static_assert(exponentBits <= 15);
diff --git a/flang/include/flang/Decimal/binary-floating-point.h b/flang/include/flang/Decimal/binary-floating-point.h
index 28346e71828fd4f..b9346a8585e2d24 100644
--- a/flang/include/flang/Decimal/binary-floating-point.h
+++ b/flang/include/flang/Decimal/binary-floating-point.h
@@ -21,10 +21,19 @@
 
 namespace Fortran::decimal {
 
+enum FortranRounding {
+  RoundNearest, /* RN and RP */
+  RoundUp, /* RU */
+  RoundDown, /* RD */
+  RoundToZero, /* RZ - no rounding */
+  RoundCompatible, /* RC: like RN, but ties go away from 0 */
+};
+
 template <int BINARY_PRECISION>
 class BinaryFloatingPointNumber : public common::RealDetails<BINARY_PRECISION> {
 public:
   using Details = common::RealDetails<BINARY_PRECISION>;
+  using Details::binaryPrecision;
   using Details::bits;
   using Details::decimalPrecision;
   using Details::decimalRange;
@@ -33,6 +42,7 @@ class BinaryFloatingPointNumber : public common::RealDetails<BINARY_PRECISION> {
   using Details::isImplicitMSB;
   using Details::maxDecimalConversionDigits;
   using Details::maxExponent;
+  using Details::maxHexadecimalConversionDigits;
   using Details::significandBits;
 
   using RawType = common::HostUnsignedIntType<bits>;
@@ -120,6 +130,55 @@ class BinaryFloatingPointNumber : public common::RealDetails<BINARY_PRECISION> {
     InsertExplicitMSB();
   }
 
+  static constexpr BinaryFloatingPointNumber Infinity(bool isNegative) {
+    RawType result{RawType{maxExponent} << significandBits};
+    if (isNegative) {
+      result |= RawType{1} << (bits - 1);
+    }
+    return BinaryFloatingPointNumber{result};
+  }
+
+  // Returns true when the result is exact
+  constexpr bool RoundToBits(int keepBits, enum FortranRounding mode) {
+    if (IsNaN() || IsInfinite() || keepBits >= binaryPrecision) {
+      return true;
+    }
+    int lostBits{binaryPrecision - keepBits};
+    RawType lostMask{static_cast<RawType>((RawType{1} << lostBits) - 1)};
+    if (RawType lost{static_cast<RawType>(raw_ & lostMask)}; lost != 0) {
+      bool increase{false};
+      switch (mode) {
+      case RoundNearest:
+        if (lost >> (lostBits - 1) != 0) { // >= tie
+          if ((lost & (lostMask >> 1)) != 0) {
+            increase = true; // > tie
+          } else {
+            increase = ((raw_ >> lostBits) & 1) != 0; // tie to even
+          }
+        }
+        break;
+      case RoundUp:
+        increase = !IsNegative();
+        break;
+      case RoundDown:
+        increase = IsNegative();
+        break;
+      case RoundToZero:
+        break;
+      case RoundCompatible:
+        increase = lost >> (lostBits - 1) != 0; // >= tie
+        break;
+      }
+      if (increase) {
+        raw_ |= lostMask;
+        Next();
+      }
+      return false; // inexact
+    } else {
+      return true; // exact
+    }
+  }
+
 private:
   constexpr void RemoveExplicitMSB() {
     if constexpr (!isImplicitMSB) {
diff --git a/flang/include/flang/Decimal/decimal.h b/flang/include/flang/Decimal/decimal.h
index b9ac6b71cd03a7f..a4e0ee7c8474607 100644
--- a/flang/include/flang/Decimal/decimal.h
+++ b/flang/include/flang/Decimal/decimal.h
@@ -43,14 +43,6 @@ struct ConversionToDecimalResult {
   enum ConversionResultFlags flags;
 };
 
-enum FortranRounding {
-  RoundNearest, /* RN and RP */
-  RoundUp, /* RU */
-  RoundDown, /* RD */
-  RoundToZero, /* RZ - no rounding */
-  RoundCompatible, /* RC: like RN, but ties go away from 0 */
-};
-
 /* The "minimize" flag causes the fewest number of output digits
  * to be emitted such that reading them back into the same binary
  * floating-point format with RoundNearest will return the same
diff --git a/flang/runtime/edit-input.cpp b/flang/runtime/edit-input.cpp
index 1861c9f8499b0b0..85096021d5193e1 100644
--- a/flang/runtime/edit-input.cpp
+++ b/flang/runtime/edit-input.cpp
@@ -229,17 +229,22 @@ bool EditIntegerInput(
 
 // Parses a REAL input number from the input source as a normalized
 // fraction into a supplied buffer -- there's an optional '-', a
-// decimal point, and at least one digit.  The adjusted exponent value
-// is returned in a reference argument.  The returned value is the number
-// of characters that (should) have been written to the buffer -- this can
-// be larger than the buffer size and can indicate overflow.  Replaces
-// blanks with zeroes if appropriate.
-static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
-    const DataEdit &edit, int &exponent) {
+// (hexa)decimal point, and at least one digit.
+// Replaces blanks with zeroes where appropriate.
+struct ScannedRealInput {
+  // Number of characters that (should) have been written to the
+  // buffer -- this can be larger than the buffer size, which
+  // indicates buffer overflow.  Zero indicates an error.
+  int got{0};
+  int exponent{0}; // adjusted as necessary; binary if isHexadecimal
+  bool isHexadecimal{false}; // 0X...
+};
+static ScannedRealInput ScanRealInput(
+    char *buffer, int bufferSize, IoStatementState &io, const DataEdit &edit) {
   std::optional<int> remaining;
   std::optional<char32_t> next;
   int got{0};
-  std::optional<int> decimalPoint;
+  std::optional<int> decimalPoint; // misnamed for hexadecimal, sorry
   auto Put{[&](char ch) -> void {
     if (got < bufferSize) {
       buffer[got] = ch;
@@ -251,6 +256,7 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
     Put('-');
   }
   bool bzMode{(edit.modes.editingFlags & blankZero) != 0};
+  int exponent{0};
   if (!next || (!bzMode && *next == ' ')) {
     if (!edit.IsListDirected() && !io.GetConnectionState().IsAtEOF()) {
       // An empty/blank field means zero when not list-directed.
@@ -259,10 +265,11 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
       // required to pass FCVS.
       Put('0');
     }
-    return got;
+    return {got, exponent, false};
   }
   char32_t decimal{GetDecimalPoint(edit)};
   char32_t first{*next >= 'a' && *next <= 'z' ? *next + 'A' - 'a' : *next};
+  bool isHexadecimal{false};
   if (first == 'N' || first == 'I') {
     // NaN or infinity - convert to upper case
     // Subtle: a blank field of digits could be followed by 'E' or 'D',
@@ -283,7 +290,7 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
         if (depth == 0) {
           break;
         } else if (!next) {
-          return 0; // error
+          return {}; // error
         } else if (*next == '(') {
           ++depth;
         } else if (*next == ')') {
@@ -292,19 +299,32 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
         Put(*next);
       }
     }
-    exponent = 0;
   } else if (first == decimal || (first >= '0' && first <= '9') ||
       (bzMode && (first == ' ' || first == '\t')) || first == 'E' ||
       first == 'D' || first == 'Q') {
-    Put('.'); // input field is normalized to a fraction
+    if (first == '0') {
+      next = io.NextInField(remaining, edit);
+      if (next && (*next == 'x' || *next == 'X')) { // 0X...
+        isHexadecimal = true;
+        next = io.NextInField(remaining, edit);
+      } else {
+        Put('0');
+      }
+    }
+    // input field is normalized to a fraction
+    if (!isHexadecimal) {
+      Put('.');
+    }
     auto start{got};
     for (; next; next = io.NextInField(remaining, edit)) {
       char32_t ch{*next};
       if (ch == ' ' || ch == '\t') {
-        if (bzMode) {
+        if (isHexadecimal) {
+          return {}; // error
+        } else if (bzMode) {
           ch = '0'; // BZ mode - treat blank as if it were zero
         } else {
-          continue;
+          continue; // ignore blank in fixed field
         }
       }
       if (ch == '0' && got == start && !decimalPoint) {
@@ -314,6 +334,10 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
       } else if (ch == decimal && !decimalPoint) {
         // the decimal point is *not* copied to the buffer
         decimalPoint = got - start; // # of digits before the decimal point
+      } else if (isHexadecimal && ch >= 'A' && ch <= 'F') {
+        Put(ch);
+      } else if (isHexadecimal && ch >= 'a' && ch <= 'f') {
+        Put(ch - 'a' + 'A'); // normalize to capitals
       } else {
         break;
       }
@@ -328,17 +352,22 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
     auto nextBeforeExponent{next};
     auto startExponent{io.GetConnectionState().positionInRecord};
     bool hasGoodExponent{false};
-    if (next &&
-        (*next == 'e' || *next == 'E' || *next == 'd' || *next == 'D' ||
-            *next == 'q' || *next == 'Q')) {
-      // Optional exponent letter.  Blanks are allowed between the
-      // optional exponent letter and the exponent value.
-      io.SkipSpaces(remaining);
-      next = io.NextInField(remaining, edit);
+    if (next) {
+      if (isHexadecimal) {
+        if (*next == 'p' || *next == 'P') {
+          next = io.NextInField(remaining, edit);
+        } else {
+          // The binary exponent is not optional in the standard.
+          return {}; // error
+        }
+      } else if (*next == 'e' || *next == 'E' || *next == 'd' || *next == 'D' ||
+          *next == 'q' || *next == 'Q') {
+        // Optional exponent letter.  Blanks are allowed between the
+        // optional exponent letter and the exponent value.
+        io.SkipSpaces(remaining);
+        next = io.NextInField(remaining, edit);
+      }
     }
-    // The default exponent is -kP, but the scale factor doesn't affect
-    // an explicit exponent.
-    exponent = -edit.modes.scale;
     if (next &&
         (*next == '-' || *next == '+' || (*next >= '0' && *next <= '9') ||
             *next == ' ' || *next == '\t')) {
@@ -346,14 +375,16 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
       if (negExpo || *next == '+') {
         next = io.NextInField(remaining, edit);
       }
-      for (exponent = 0; next; next = io.NextInField(remaining, edit)) {
+      for (; next; next = io.NextInField(remaining, edit)) {
         if (*next >= '0' && *next <= '9') {
           hasGoodExponent = true;
           if (exponent < 10000) {
             exponent = 10 * exponent + *next - '0';
           }
         } else if (*next == ' ' || *next == '\t') {
-          if (bzMode) {
+          if (isHexadecimal) {
+            break;
+          } else if (bzMode) {
             hasGoodExponent = true;
             exponent = 10 * exponent;
           }
@@ -366,11 +397,21 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
       }
     }
     if (!hasGoodExponent) {
+      if (isHexadecimal) {
+        return {}; // error
+      }
       // There isn't a good exponent; do not consume it.
       next = nextBeforeExponent;
       io.HandleAbsolutePosition(startExponent);
-    }
-    if (decimalPoint) {
+      // The default exponent is -kP, but the scale factor doesn't affect
+      // an explicit exponent.
+      exponent = -edit.modes.scale;
+    }
+    // Adjust exponent by number of digits before the decimal point.
+    if (isHexadecimal) {
+      // Exponents for hexadecimal input are binary.
+      exponent += decimalPoint.value_or(got - start) * 4;
+    } else if (decimalPoint) {
       exponent += *decimalPoint;
     } else {
       // When no decimal point (or comma) appears in the value, the 'd'
@@ -379,10 +420,6 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
       // the assumed decimal point (13.7.2.3.2)
       exponent += got - start - edit.digits.value_or(0);
     }
-  } else {
-    // TODO: hex FP input
-    exponent = 0;
-    return 0;
   }
   // Consume the trailing ')' of a list-directed or NAMELIST complex
   // input value.
@@ -403,10 +440,10 @@ static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
       next = io.NextInField(remaining, edit);
     }
     if (next) {
-      return 0; // error: unused nonblank character in fixed-width field
+      return {}; // error: unused nonblank character in fixed-width field
     }
   }
-  return got;
+  return {got, exponent, isHexadecimal};
 }
 
 static void RaiseFPExceptions(decimal::ConversionResultFlags flags) {
@@ -433,7 +470,7 @@ static void RaiseFPExceptions(decimal::ConversionResultFlags flags) {
 // converter without modification, this fast path for real input
 // saves time by avoiding memory copies and reformatting of the exponent.
 template <int PRECISION>
-static bool TryFastPathRealInput(
+static bool TryFastPathRealDecimalInput(
     IoStatementState &io, const DataEdit &edit, void *n) {
   if (edit.modes.editingFlags & (blankZero | decimalComma)) {
     return false;
@@ -504,10 +541,101 @@ static bool TryFastPathRealInput(
   return true;
 }
 
+template <int binaryPrecision>
+decimal::ConversionToBinaryResult<binaryPrecision> ConvertHexadecimal(
+    const char *&p, enum decimal::FortranRounding rounding, int expo) {
+  using RealType = decimal::BinaryFloatingPointNumber<binaryPrecision>;
+  using RawType = typename RealType::RawType;
+  bool isNegative{*p == '-'};
+  constexpr RawType one{1};
+  RawType signBit{0};
+  if (isNegative) {
+    ++p;
+    signBit = one << (RealType::bits - 1);
+  }
+  RawType fraction{0};
+  // Adjust the incoming binary P+/- exponent to shift the radix point
+  // to below the LSB and add in the bias.
+  expo += binaryPrecision + 1 + RealType::exponentBias;
+  // Input the fraction.  Maintain two extra least-significant bits for
+  // rounding.
+  for (; *p; ++p) {
+    fraction <<= 4;
+    expo -= 4;
+    if (*p >= '0' && *p <= '9') {
+      fraction |= *p - '0';
+    } else if (*p >= 'A' && *p <= 'F') {
+      fraction |= *p - 'A' + 10; // data were normalized to capitals
+    } else {
+      break;
+    }
+    while (fraction >> (binaryPrecision + 2)) {
+      fraction = (fraction >> 1) | (fraction & 1);
+      ++expo;
+    }
+  }
+  int roundingBits{0};
+  if (fraction) {
+    // Boost biased expo if too small
+    while (expo < 1) {
+      fraction = (fraction >> 1) | (fraction & one);
+      ++expo;
+    }
+    // Normalize
+    while (expo > 1 && !(fraction >> (binaryPrecision + 2 - 1))) {
+      fraction <<= 1;
+      --expo;
+    }
+    // Rounding
+    roundingBits = static_cast<int>(fraction) & 3;
+    fraction >>= 2;
+    bool increase{false};
+    switch (rounding) {
+    case decimal::RoundNearest: // RN & RP
+      increase = roundingBits == 3 || (roundingBits == 2 && fraction & 1);
+      break;
+    case decimal::RoundUp: // RU
+      increase = !isNegative && roundingBits > 0;
+      break;
+    case decimal::RoundDown: // RD
+      increase = isNegative && roundingBits > 0;
+      break;
+    case decimal::RoundToZero: // RZ
+      break;
+    case decimal::RoundCompatible: // RC
+      increase = roundingBits >= 2;
+      break;
+    }
+    if (increase) {
+      ++fraction;
+      if (fraction >> binaryPrecision) {
+        fraction >>= 1;
+        ++expo;
+      }
+    }
+  }
+  // Package & return result
+  constexpr RawType significandMask{(one << RealType::significandBits) - 1};
+  if (!fraction) {
+    expo = 0;
+  } else if (expo == 1 && !(fraction >> (binaryPrecision - 1))) {
+    expo = 0; // subnormal
+  } else if (expo >= RealType::maxExponent) {
+    expo = RealType::maxExponent; // +/-Inf
+    fraction = 0;
+  } else {
+    fraction &= significandMask; // remove explicit normalization unless x87
+  }
+  return decimal::ConversionToBinaryResult<binaryPrecision>{
+      RealType{static_cast<RawType>(signBit |
+          static_cast<RawType>(expo) << RealType::significandBits | fraction)},
+      roundingBits ? decimal::Inexact : decimal::Exact};
+}
+
 template <int KIND>
 bool EditCommonRealInput(IoStatementState &io, const DataEdit &edit, void *n) {
   constexpr int binaryPrecision{common::PrecisionOfRealKind(KIND)};
-  if (TryFastPathRealInput<binaryPrecision>(io, edit, n)) {
+  if (TryFastPathRealDecimalInput<binaryPrecision>(io, edit, n)) {
     return CheckCompleteListDirectedField(io, edit);
   }
   // Fast path wasn't available or didn't work; go the more general route
@@ -515,8 +643,8 @@ bool EditCommonRealInput(IoStatementState &io, const DataEdit &edit, void *n) {
       common::MaxDecimalConversionDigits(binaryPrecision)};
   static constexpr int bufferSize{maxDigits + 18};
   char buffer[bufferSize];
-  int exponent{0};
-  int got{ScanRealInput(buffer, maxDigits + 2, io, edit, exponent)};
+  auto scanned{ScanRealInput(buffer, maxDigits + 2, io, edit)};
+  int got{scanned.got};
   if (got >= maxDigits + 2) {
     io.GetIoErrorHandler().Crash("EditCommonRealInput: buffer was too small");
     return false;
@@ -529,48 +657,55 @@ bool EditCommonRealInput(IoStatementState &io, const DataEdit &edit, void *n) {
         static_cast<int>(connection.currentRecordNumber));
     return false;
   }
-  bool hadExtra{got > maxDigits};
-  if (exponent != 0) {
-    buffer[got++] = 'e';
-    if (exponent < 0) {
-      buffer[got++] = '-';
-      exponent = -exponent;
-    }
-    if (exponent > 9999) {
-      exponent = 9999; // will convert to +/-Inf
-    }
-    if (exponent > 999) {
-      int dig{exponent / 1000};
-      buffer[got++] = '0' + dig;
-      int rest{exponent - 1000 * dig};
-      dig = rest / 100;
-      buffer[got++] = '0' + dig;
-      rest -= 100 * dig;
-      dig = rest / 10;
-      buffer[got++] = '0' + dig;
-      buffer[got++] = '0' + (rest - 10 * dig);
-    } else if (exponent > 99) {
-      int dig{exponent / 100};
-      buffer[got++] = '0' + dig;
-      int rest{exponent - 100 * dig};
-      dig = rest / 10;
-      buffer[got++] = '0' + dig;
-      buffer[got++] = '0' + (rest - 10 * dig);
-    } else if (exponent > 9) {
-      int dig{exponent / 10};
-      buffer[got++] = '0' + dig;
-      buffer[got++] = '0' + (exponent - 10 * dig);
-    } else {
-      buffer[got++] = '0' + exponent;
-    }
-  }
-  buffer[got] = '\0';
+  decimal::ConversionToBinaryResult<binaryPrecision> converted;
   const char *p{buffer};
-  decimal::ConversionToBinaryResult<binaryPrecision> converted{
-      decimal::ConvertToBinary<binaryPrecision>(p, edit.modes.round)};
-  if (hadExtra) {
-    converted.flags = static_cast<enum decimal::ConversionResultFlags>(
-        converted.flags | decimal::Inexact);
+  if (scanned.isHexadecimal) {
+    buffer[got] = '\0';
+    converted = ConvertHexadecimal<binaryPrecision>(
+        p, edit.modes.round, scanned.exponent);
+  } else {
+    bool hadExtra{got > maxDigits};
+    int exponent{scanned.exponent};
+    if (exponent != 0) {
+      buffer[got++] = 'e';
+      if (exponent < 0) {
+        buffer[got++] = '-';
+        exponent = -exponent;
+      }
+      if (exponent > 9999) {
+        exponent = 9999; // will convert to +/-Inf
+      }
+      if (exponent > 999) {
+        int dig{exponent / 1000};
+        buffer[got++] = '0' + dig;
+        int rest{exponent - 1000 * dig};
+        dig = rest / 100;
+        buffer[got++] = '0' + dig;
+        rest -= 100 * dig;
+        dig = rest / 10;
+        buffer[got++] = '0' + dig;
+        buffer[got++] = '0' + (rest - 10 * dig);
+      } else if (exponent > 99) {
+        int dig{exponent / 100};
+        buffer[got++] = '0' + dig;
+        int rest{exponent - 100 * dig};
+        dig = rest / 10;
+        buffer[got++] = '0' + dig;
+        buffer[got++] = '0' + (rest - 10 * dig);
+      } else if (exponent > 9) {
+        int dig{exponent / 10};
+        buffer[got++] = '0' + dig;
+        buffer[got++] = '0' + (exponent - 10 * dig);
+      } else {
+        buffer[got++] = '0' + exponent;
+      }
+    }
+    buffer[got] = '\0';
+    converted = decimal::ConvertToBinary<binaryPrecision>(p, edit.modes.round);
+    if (hadExtra) {
+      converted.flags = static_cast<enum decimal::ConversionResultFlags>(
+          converted.flags | decimal::Inexact);
+    }
   }
   if (*p) { // unprocessed junk after value
     const auto &connection{io.GetConnectionState()};
diff --git a/flang/runtime/edit-output.cpp b/flang/runtime/edit-output.cpp
index be0bb07f08bfe7b..40e1c593a722f0e 100644
--- a/flang/runtime/edit-output.cpp
+++ b/flang/runtime/edit-output.cpp
@@ -205,13 +205,20 @@ const char *RealOutputEditingBase::FormatExponent(
     } else if (exponent == eEnd) {
       *--exponent = '0'; // Ew.dE0 with zero-valued exponent
     }
-  } else { // ensure at least two exponent digits
+  } else if (edit.variation == 'X') {
+    if (expo == 0) {
+      *--exponent = '0'; // EX without Ee and zero-valued exponent
+    }
+  } else {
+    // Ensure at least two exponent digits unless EX
     while (exponent + 2 > eEnd) {
       *--exponent = '0';
     }
   }
   *--exponent = expo < 0 ? '-' : '+';
-  if (edit.expoDigits || edit.IsListDirected() || exponent + 3 == eEnd) {
+  if (edit.variation == 'X') {
+    *--exponent = 'P';
+  } else if (edit.expoDigits || edit.IsListDirected() || exponent + 3 == eEnd) {
     *--exponent = edit.descriptor == 'D' ? 'D' : 'E'; // not 'G' or 'Q'
   }
   length = eEnd - exponent;
@@ -251,19 +258,32 @@ bool RealOutputEditingBase::EmitSuffix(const DataEdit &edit) {
 }
 
 template <int KIND>
-decimal::ConversionToDecimalResult RealOutputEditing<KIND>::Convert(
+decimal::ConversionToDecimalResult RealOutputEditing<KIND>::ConvertToDecimal(
     int significantDigits, enum decimal::FortranRounding rounding, int flags) {
   auto converted{decimal::ConvertToDecimal<binaryPrecision>(buffer_,
       sizeof buffer_, static_cast<enum decimal::DecimalConversionFlags>(flags),
       significantDigits, rounding, x_)};
   if (!converted.str) { // overflow
     io_.GetIoErrorHandler().Crash(
-        "RealOutputEditing::Convert : buffer size %zd was insufficient",
+        "RealOutputEditing::ConvertToDecimal: buffer size %zd was insufficient",
         sizeof buffer_);
   }
   return converted;
 }
 
+static bool IsInfOrNaN(const char *p, int length) {
+  if (!p || length < 1) {
+    return false;
+  }
+  if (*p == '-' || *p == '+') {
+    if (length == 1) {
+      return false;
+    }
+    ++p;
+  }
+  return *p == 'I' || *p == 'N';
+}
+
 // 13.7.2.3.3 in F'2018
 template <int KIND>
 bool RealOutputEditing<KIND>::EditEorDOutput(const DataEdit &edit) {
@@ -275,7 +295,6 @@ bool RealOutputEditing<KIND>::EditEorDOutput(const DataEdit &edit) {
   if (edit.modes.editingFlags & signPlus) {
     flags |= decimal::AlwaysSign;
   }
-  bool noLeadingSpaces{editWidth == 0};
   int scale{edit.modes.scale}; // 'kP' value
   if (editWidth == 0) { // "the processor selects the field width"
     if (edit.digits.has_value()) { // E0.d
@@ -319,8 +338,8 @@ bool RealOutputEditing<KIND>::EditEorDOutput(const DataEdit &edit) {
   // In EN editing, multiple attempts may be necessary, so this is a loop.
   while (true) {
     decimal::ConversionToDecimalResult converted{
-        Convert(significantDigits, edit.modes.round, flags)};
-    if (IsInfOrNaN(converted)) {
+        ConvertToDecimal(significantDigits, edit.modes.round, flags)};
+    if (IsInfOrNaN(converted.str, static_cast<int>(converted.length))) {
       return editWidth > 0 &&
               converted.length > static_cast<std::size_t>(editWidth)
           ? EmitRepeated(io_, '*', editWidth)
@@ -380,7 +399,7 @@ bool RealOutputEditing<KIND>::EditEorDOutput(const DataEdit &edit) {
       zeroesBeforePoint = 1;
       ++totalLength;
     }
-    if (totalLength < width && noLeadingSpaces) {
+    if (totalLength < width && editWidth == 0) {
       width = totalLength;
     }
     return EmitPrefix(edit, totalLength, width) &&
@@ -418,8 +437,8 @@ bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
   bool canIncrease{true};
   while (true) {
     decimal::ConversionToDecimalResult converted{
-        Convert(extraDigits + fracDigits, rounding, flags)};
-    if (IsInfOrNaN(converted)) {
+        ConvertToDecimal(extraDigits + fracDigits, rounding, flags)};
+    if (IsInfOrNaN(converted.str, static_cast<int>(converted.length))) {
       return editWidth > 0 &&
               converted.length > static_cast<std::size_t>(editWidth)
           ? EmitRepeated(io_, '*', editWidth)
@@ -521,8 +540,8 @@ DataEdit RealOutputEditing<KIND>::EditForGOutput(DataEdit edit) {
     flags |= decimal::AlwaysSign;
   }
   decimal::ConversionToDecimalResult converted{
-      Convert(significantDigits, edit.modes.round, flags)};
-  if (IsInfOrNaN(converted)) {
+      ConvertToDecimal(significantDigits, edit.modes.round, flags)};
+  if (IsInfOrNaN(converted.str, static_cast<int>(converted.length))) {
     return edit; // Inf/Nan -> Ew.d (same as Fw.d)
   }
   int expo{IsZero() ? 1 : converted.decimalExponent}; // 's'
@@ -549,8 +568,9 @@ DataEdit RealOutputEditing<KIND>::EditForGOutput(DataEdit edit) {
 // 13.10.4 in F'2018
 template <int KIND>
 bool RealOutputEditing<KIND>::EditListDirectedOutput(const DataEdit &edit) {
-  decimal::ConversionToDecimalResult converted{Convert(1, edit.modes.round)};
-  if (IsInfOrNaN(converted)) {
+  decimal::ConversionToDecimalResult converted{
+      ConvertToDecimal(1, edit.modes.round)};
+  if (IsInfOrNaN(converted.str, static_cast<int>(converted.length))) {
     return EditEorDOutput(edit);
   }
   int expo{converted.decimalExponent};
@@ -567,11 +587,119 @@ bool RealOutputEditing<KIND>::EditListDirectedOutput(const DataEdit &edit) {
   return EditFOutput(edit);
 }
 
-// 13.7.5.2.6 in F'2018
+// 13.7.2.3.6 in F'2023
+// The specification for hexadecimal output, unfortunately for implementors,
+// leaves as "implementation dependent" the choice of how to emit values
+// with multiple hexadecimal output possibilities that are numerically
+// equivalent.  The one working implementation of EX output that I can find
+// apparently chooses to frame the nybbles from most to least significant,
+// rather than trying to minimize the magnitude of the binary exponent.
+// E.g., 2. is edited into 0X8.0P-2 rather than 0X2.0P0.  This implementation
+// follows that precedent so as to avoid a gratuitous incompatibility.
 template <int KIND>
-bool RealOutputEditing<KIND>::EditEXOutput(const DataEdit &) {
-  io_.GetIoErrorHandler().Crash(
-      "not yet implemented: EX output editing"); // TODO
+auto RealOutputEditing<KIND>::ConvertToHexadecimal(
+    int significantDigits, enum decimal::FortranRounding rounding, int flags)
+    -> ConvertToHexadecimalResult {
+  if (x_.IsNaN() || x_.IsInfinite()) {
+    auto converted{ConvertToDecimal(significantDigits, rounding, flags)};
+    return {converted.str, static_cast<int>(converted.length), 0};
+  }
+  x_.RoundToBits(4 * significantDigits, rounding);
+  if (x_.IsInfinite()) { // rounded away to +/-Inf
+    auto converted{ConvertToDecimal(significantDigits, rounding, flags)};
+    return {converted.str, static_cast<int>(converted.length), 0};
+  }
+  int len{0};
+  if (x_.IsNegative()) {
+    buffer_[len++] = '-';
+  } else if (flags & decimal::AlwaysSign) {
+    buffer_[len++] = '+';
+  }
+  auto fraction{x_.Fraction()};
+  if (fraction == 0) {
+    buffer_[len++] = '0';
+    return {buffer_, len, 0};
+  } else {
+    // Ensure that the MSB is set.
+    int expo{x_.UnbiasedExponent() - 3};
+    while (!(fraction >> (x_.binaryPrecision - 1))) {
+      fraction <<= 1;
+      --expo;
+    }
+    // This is initially the right shift count needed to bring the
+    // most-significant hexadecimal digit's bits into the LSBs.
+    // x_.binaryPrecision is constant, so / can be used for readability.
+    int shift{x_.binaryPrecision - 4};
+    typename BinaryFloatingPoint::RawType one{1};
+    auto remaining{(one << shift) - one};
+    for (int digits{0}; digits < significantDigits; ++digits) {
+      if ((flags & decimal::Minimize) && !(fraction & remaining)) {
+        break;
+      }
+      int hexDigit{0};
+      if (shift >= 0) {
+        hexDigit = (fraction >> shift) & 0xf;
+      } else if (shift >= -3) {
+        hexDigit = (fraction << -shift) & 0xf;
+      }
+      if (hexDigit >= 10) {
+        buffer_[len++] = 'A' + hexDigit - 10;
+      } else {
+        buffer_[len++] = '0' + hexDigit;
+      }
+      shift -= 4;
+      remaining >>= 4;
+    }
+    return {buffer_, len, expo};
+  }
+}
+
+template <int KIND>
+bool RealOutputEditing<KIND>::EditEXOutput(const DataEdit &edit) {
+  addSpaceBeforeCharacter(io_);
+  int editDigits{edit.digits.value_or(0)}; // 'd' field
+  int significantDigits{editDigits + 1};
+  int flags{0};
+  if (edit.modes.editingFlags & signPlus) {
+    flags |= decimal::AlwaysSign;
+  }
+  int editWidth{edit.width.value_or(0)}; // 'w' field
+  if (editWidth == 0 && !edit.digits) { // EX0 (no .d)
+    flags |= decimal::Minimize;
+    significantDigits = 28; // enough for 128-bit F.P.
+  }
+  auto converted{
+      ConvertToHexadecimal(significantDigits, edit.modes.round, flags)};
+  if (IsInfOrNaN(converted.str, converted.length)) {
+    return editWidth > 0 && converted.length > editWidth
+        ? EmitRepeated(io_, '*', editWidth)
+        : EmitRepeated(io_, ' ', editWidth - converted.length) &&
+            EmitAscii(io_, converted.str, converted.length);
+  }
+  int signLength{converted.length > 0 &&
+              (converted.str[0] == '-' || converted.str[0] == '+')
+          ? 1
+          : 0};
+  int convertedDigits{converted.length - signLength};
+  int expoLength{0};
+  const char *exponent{FormatExponent(converted.exponent, edit, expoLength)};
+  int trailingZeroes{flags & decimal::Minimize
+          ? 0
+          : std::max(0, significantDigits - convertedDigits)};
+  int totalLength{converted.length + trailingZeroes + expoLength + 3 /*0X.*/};
+  int width{editWidth > 0 ? editWidth : totalLength};
+  return totalLength > width || !exponent
+      ? EmitRepeated(io_, '*', width)
+      : EmitRepeated(io_, ' ', width - totalLength) &&
+          EmitAscii(io_, converted.str, signLength) &&
+          EmitAscii(io_, "0X", 2) &&
+          EmitAscii(io_, converted.str + signLength, 1) &&
+          EmitAscii(
+              io_, edit.modes.editingFlags & decimalComma ? "," : ".", 1) &&
+          EmitAscii(io_, converted.str + signLength + 1,
+              converted.length - (signLength + 1)) &&
+          EmitRepeated(io_, '0', trailingZeroes) &&
+          EmitAscii(io_, exponent, expoLength);
 }
 
 template <int KIND> bool RealOutputEditing<KIND>::Edit(const DataEdit &edit) {
diff --git a/flang/runtime/edit-output.h b/flang/runtime/edit-output.h
index 765e41f89827de7..4e6d6b25b4dd2d3 100644
--- a/flang/runtime/edit-output.h
+++ b/flang/runtime/edit-output.h
@@ -38,20 +38,6 @@ class RealOutputEditingBase {
 protected:
   explicit RealOutputEditingBase(IoStatementState &io) : io_{io} {}
 
-  static bool IsInfOrNaN(const decimal::ConversionToDecimalResult &res) {
-    const char *p{res.str};
-    if (!p || res.length < 1) {
-      return false;
-    }
-    if (*p == '-' || *p == '+') {
-      if (res.length == 1) {
-        return false;
-      }
-      ++p;
-    }
-    return *p < '0' || *p > '9';
-  }
-
   // Returns null when the exponent overflows a fixed-size output field.
   const char *FormatExponent(int, const DataEdit &edit, int &length);
   bool EmitPrefix(const DataEdit &, std::size_t length, std::size_t width);
@@ -84,7 +70,15 @@ template <int KIND> class RealOutputEditing : public RealOutputEditingBase {
 
   bool IsZero() const { return x_.IsZero(); }
 
-  decimal::ConversionToDecimalResult Convert(
+  decimal::ConversionToDecimalResult ConvertToDecimal(
+      int significantDigits, enum decimal::FortranRounding, int flags = 0);
+
+  struct ConvertToHexadecimalResult {
+    const char *str;
+    int length;
+    int exponent;
+  };
+  ConvertToHexadecimalResult ConvertToHexadecimal(
       int significantDigits, enum decimal::FortranRounding, int flags = 0);
 
   BinaryFloatingPoint x_;
diff --git a/flang/unittests/Runtime/NumericalFormatTest.cpp b/flang/unittests/Runtime/NumericalFormatTest.cpp
index 833b16be0fc3f46..219947fe4fbbb79 100644
--- a/flang/unittests/Runtime/NumericalFormatTest.cpp
+++ b/flang/unittests/Runtime/NumericalFormatTest.cpp
@@ -290,6 +290,8 @@ TEST(IOApiTests, FormatZeroes) {
       {"(1P,G32.17,';')", "          0.0000000000000000    ;"},
       {"(2P,E32.17,';')", "         00.0000000000000000E+00;"},
       {"(-1P,E32.17,';')", "         0.00000000000000000E+00;"},
+      {"(EX32.17,';')", "        0X0.00000000000000000P+0;"},
+      {"(DC,EX32.17,';')", "        0X0,00000000000000000P+0;"},
       {"(G0,';')", "0.;"},
   };
 
@@ -321,6 +323,8 @@ TEST(IOApiTests, FormatOnes) {
       {"(2P,G32.17,';')", "          1.0000000000000000    ;"},
       {"(-1P,E32.17,';')", "         0.01000000000000000E+02;"},
       {"(-1P,G32.17,';')", "          1.0000000000000000    ;"},
+      {"(EX32.17,';')", "        0X8.00000000000000000P-3;"},
+      {"(DC,EX32.17,';')", "        0X8,00000000000000000P-3;"},
       {"(G0,';')", "1.;"},
   };
 
@@ -337,6 +341,7 @@ TEST(IOApiTests, FormatNegativeOnes) {
       {"(E32.17,';')", "        -0.10000000000000000E+01;"},
       {"(F32.17,';')", "            -1.00000000000000000;"},
       {"(G32.17,';')", "         -1.0000000000000000    ;"},
+      {"(EX32.17,';')", "       -0X8.00000000000000000P-3;"},
       {"(G0,';')", "-1.;"},
   };
   for (auto const &[format, expect] : negOnes) {
@@ -365,6 +370,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(G8.1,';')", " -0.    ;"},
               {"(G0,';')", "-0.;"},
               {"(E9.1,';')", " -0.0E+00;"},
+              {"(EX9.1,';')", "-0X0.0P+0;"},
           }},
       {// +Inf
           0x7ff0000000000000,
@@ -372,9 +378,11 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(E9.1,';')", "      Inf;"},
               {"(F9.1,';')", "      Inf;"},
               {"(G9.1,';')", "      Inf;"},
+              {"(EX9.1,';')", "      Inf;"},
               {"(SP,E9.1,';')", "     +Inf;"},
               {"(SP,F9.1,';')", "     +Inf;"},
               {"(SP,G9.1,';')", "     +Inf;"},
+              {"(SP,EX9.1,';')", "     +Inf;"},
               {"(G0,';')", "Inf;"},
           }},
       {// -Inf
@@ -383,6 +391,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(E9.1,';')", "     -Inf;"},
               {"(F9.1,';')", "     -Inf;"},
               {"(G9.1,';')", "     -Inf;"},
+              {"(EX9.1,';')", "     -Inf;"},
               {"(G0,';')", "-Inf;"},
           }},
       {// NaN
@@ -391,6 +400,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(E9.1,';')", "      NaN;"},
               {"(F9.1,';')", "      NaN;"},
               {"(G9.1,';')", "      NaN;"},
+              {"(EX9.1,';')", "      NaN;"},
               {"(G0,';')", "NaN;"},
           }},
       {// NaN (sign irrelevant)
@@ -402,6 +412,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(SP,E9.1,';')", "      NaN;"},
               {"(SP,F9.1,';')", "      NaN;"},
               {"(SP,G9.1,';')", "      NaN;"},
+              {"(SP,EX9.1,';')", "      NaN;"},
               {"(G0,';')", "NaN;"},
           }},
       {// 0.1 rounded
@@ -429,6 +440,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(G0.55,';')",
                   ".1000000000000000055511151231257827021181583404541015625;"},
               {"(G0,';')", ".1;"},
+              {"(EX20.12,';')", " 0XC.CCCCCCCCCCCDP-7;"},
           }},
       {// 1.5
           0x3ff8000000000000,
@@ -436,6 +448,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(E9.2,';')", " 0.15E+01;"},
               {"(F4.1,';')", " 1.5;"},
               {"(G7.1,';')", " 2.    ;"},
+              {"(EX9.1,';')", " 0XC.0P-3;"},
               {"(RN,E8.1,';')", " 0.2E+01;"},
               {"(RN,F3.0,';')", " 2.;"},
               {"(RN,G7.0,';')", " 0.E+01;"},
@@ -465,6 +478,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(RU,E8.1,';')", "-0.1E+01;"},
               {"(RZ,E8.1,';')", "-0.1E+01;"},
               {"(RC,E8.1,';')", "-0.2E+01;"},
+              {"(EX9.1,';')", "-0XC.0P-3;"},
           }},
       {// 2.5
           0x4004000000000000,
@@ -475,6 +489,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(RU,E8.1,';')", " 0.3E+01;"},
               {"(RZ,E8.1,';')", " 0.2E+01;"},
               {"(RC,E8.1,';')", " 0.3E+01;"},
+              {"(EX9.1,';')", " 0XA.0P-2;"},
           }},
       {// -2.5
           0xc004000000000000,
@@ -485,6 +500,7 @@ TEST(IOApiTests, FormatDoubleValues) {
               {"(RU,E8.1,';')", "-0.2E+01;"},
               {"(RZ,E8.1,';')", "-0.2E+01;"},
               {"(RC,E8.1,';')", "-0.3E+01;"},
+              {"(EX9.1,';')", "-0XA.0P-2;"},
           }},
       {// least positive nonzero subnormal
           1,
@@ -583,6 +599,7 @@ TEST(IOApiTests, FormatDoubleValues) {
                   "701797267771758512566055119913150489110145103786273816725095"
                   "583738973359899366480994116420570263709027924276754456522908"
                   "753868250641971826553344726563-323;"},
+              {"(EX24.13,';')", " 0X8.0000000000000P-1077;"},
           }},
       {// least positive nonzero normal
           0x10000000000000,
@@ -603,6 +620,7 @@ TEST(IOApiTests, FormatDoubleValues) {
                   "61364675687023986783152906809846172109246253967285156250-"
                   "307;"},
               {"(G0,';')", ".22250738585072014E-307;"},
+              {"(EX24.13,';')", " 0X8.0000000000000P-1025;"},
           }},
       {// greatest finite
           0x7fefffffffffffffuLL,
@@ -633,6 +651,31 @@ TEST(IOApiTests, FormatDoubleValues) {
                   "123348274797826204144723168738177180919299881250404026184124"
                   "8583680000+306;"},
               {"(G0,';')", ".17976931348623157E+309;"},
+              {"(EX24.13,';')", " 0XF.FFFFFFFFFFFF8P+1020;"},
+          }},
+      {// EX rounding
+          0x3ff1000000000000uLL, // 1.0625
+          {
+              {"(F7.4,';')", " 1.0625;"},
+              {"(EX9.1,';')", " 0X8.8P-3;"},
+              {"(EX9.0,';')", "  0X8.P-3;"},
+              {"(RN,EX9.0,';')", "  0X8.P-3;"},
+              {"(RU,EX9.0,';')", "  0X9.P-3;"},
+              {"(RD,EX9.0,';')", "  0X8.P-3;"},
+              {"(RZ,EX9.0,';')", "  0X8.P-3;"},
+              {"(RC,EX9.0,';')", "  0X9.P-3;"},
+          }},
+      {// EX rounding
+          0xbff1000000000000uLL, // -1.0625
+          {
+              {"(F7.4,';')", "-1.0625;"},
+              {"(EX9.1,';')", "-0X8.8P-3;"},
+              {"(EX9.0,';')", " -0X8.P-3;"},
+              {"(RN,EX9.0,';')", " -0X8.P-3;"},
+              {"(RU,EX9.0,';')", " -0X8.P-3;"},
+              {"(RD,EX9.0,';')", " -0X9.P-3;"},
+              {"(RZ,EX9.0,';')", " -0X8.P-3;"},
+              {"(RC,EX9.0,';')", " -0X9.P-3;"},
           }},
   };
 
@@ -775,11 +818,11 @@ TEST(IOApiTests, FormatIntegerValues) {
 }
 
 //------------------------------------------------------------------------------
-/// Tests for input formatting real values
+/// Tests for input editing real values
 //------------------------------------------------------------------------------
 
 // Ensure double input values correctly map to raw uint64 values
-TEST(IOApiTests, FormatDoubleInputValues) {
+TEST(IOApiTests, EditDoubleInputValues) {
   using TestCaseTy = std::tuple<const char *, const char *, std::uint64_t>;
   static const std::vector<TestCaseTy> testCases{
       {"(F18.0)", "                 0", 0x0},
@@ -806,6 +849,21 @@ TEST(IOApiTests, FormatDoubleInputValues) {
       {"(BZ,F18.0)", "           .      ", 0x0},
       {"(BZ,F18.0)", "           . e +1 ", 0x0},
       {"(DC,F18.0)", "              12,5", 0x4029000000000000},
+      {"(EX22.0)", "0X0P0                 ", 0x0}, // +0.
+      {"(EX22.0)", "-0X0P0                ", 0x8000000000000000}, // -0.
+      {"(EX22.0)", "0X.8P1                ", 0x3ff0000000000000}, // 1.0
+      {"(EX22.0)", "0X8.P-3               ", 0x3ff0000000000000}, // 1.0
+      {"(EX22.0)", "0X.1P4                ", 0x3ff0000000000000}, // 1.0
+      {"(EX22.0)", "0X10.P-4              ", 0x3ff0000000000000}, // 1.0
+      {"(EX22.0)", "0X8.00P-3             ", 0x3ff0000000000000}, // 1.0
+      {"(EX22.0)", "0X80.0P-6             ", 0x4000000000000000}, // 2.0
+      {"(EX22.0)", "0XC.CCCCCCCCCCCDP-7   ", 0x3fb999999999999a}, // 0.1
+      {"(EX22.0)", "0X.8P-1021            ", 0x0010000000000000}, // min normal
+      {"(EX22.0)", "0X.8P-1022            ", 0x0008000000000000}, // subnormal
+      {"(EX22.0)", "0X.8P-1073            ", 0x0000000000000001}, // min subn.
+      {"(EX22.0)", "0X.FFFFFFFFFFFFF8P1024", 0x7fefffffffffffff}, // max finite
+      {"(EX22.0)", "0X.8P1025             ", 0x7ff0000000000000}, // +Inf
+      {"(EX22.0)", "-0X.8P1025            ", 0xfff0000000000000}, // -Inf
   };
   for (auto const &[format, data, want] : testCases) {
     auto cookie{IONAME(BeginInternalFormattedInput)(



More information about the flang-commits mailing list