[PATCH] Fortran: [PR93727] Add EX format READ (read_ex)

Jerry D <[email protected]>
Newsgroups gmane.comp.gcc.patches,gmane.comp.gcc.fortran
Message-ID <[email protected]>
The following implements the last of the EX formatting work. The attached patch 
has been regression tested on x86_64. The test case provided round trip testing 
to ensure what we read is what we wrote.

OK for mainline?

Best regards,

Jerry
---
Fortran: [PR93727] Add EX format READ (read_ex)

Implement read_ex in libgfortran to handle EX edit-descriptor input
per Fortran 2023 13.7.2.3.6.  The input field may contain:

   * a hexadecimal-significand form (0X<sig>P<exp>) passed directly
     to the C strtod/strtold family for exact bit-for-bit conversion;
   * any form acceptable for Fw.d input (decimal fallback), including
     INF and NAN representations.

For decimal input without a decimal point the d field of the EX.w.d
descriptor adjusts the exponent exactly as for Fw.d.  BN/BZ blank
handling and the kP scale factor are also supported via the shared
decimal path.

Assisted by: Claude Sonnet 4.6

	PR fortran/93727

libgfortran/ChangeLog:

	* io/read.c (read_ex): New function implementing EX format read.
	* io/io.h (read_ex): Declare.
	* io/transfer.c (formatted_transfer_scalar_read): Add FMT_EX case
	dispatching to read_ex.

gcc/testsuite/ChangeLog:

	* gfortran.dg/EXformat_4.F90: New test covering EX format read
	for kind=4 and 8 (always) and kind=10, 16 (when available):
	hex-significand literals, lowercase prefix, negative binary
	exponent, decimal fallback, d-field adjustment, INF/NAN, zero
	field, and round-trips through write_ex.
---
0001-Fortran-PR93727-Add-EX-format-READ-read_ex.patch (text/x-patch, 13.6 KB)
From ac5caffca8de75c6ad6026d930c1bc8a38ee0452 Mon Sep 17 00:00:00 2001
From: Jerry DeLisle <[email protected]>
Date: Mon, 25 May 2026 21:13:19 -0700
Subject: [PATCH] Fortran: [PR93727] Add EX format READ (read_ex)

Implement read_ex in libgfortran to handle EX edit-descriptor input
per Fortran 2023 13.7.2.3.6.  The input field may contain:

  * a hexadecimal-significand form (0X<sig>P<exp>) passed directly
    to the C strtod/strtold family for exact bit-for-bit conversion;
  * any form acceptable for Fw.d input (decimal fallback), including
    INF and NAN representations.

For decimal input without a decimal point the d field of the EX.w.d
descriptor adjusts the exponent exactly as for Fw.d.  BN/BZ blank
handling and the kP scale factor are also supported via the shared
decimal path.

Assisted by: Claude Sonnet 4.6

	PR fortran/93727

libgfortran/ChangeLog:

	* io/read.c (read_ex): New function implementing EX format read.
	* io/io.h (read_ex): Declare.
	* io/transfer.c (formatted_transfer_scalar_read): Add FMT_EX case
	dispatching to read_ex.

gcc/testsuite/ChangeLog:

	* gfortran.dg/EXformat_4.F90: New test covering EX format read
	for kind=4 and 8 (always) and kind=10, 16 (when available):
	hex-significand literals, lowercase prefix, negative binary
	exponent, decimal fallback, d-field adjustment, INF/NAN, zero
	field, and round-trips through write_ex.
---
 gcc/testsuite/gfortran.dg/EXformat_4.F90 | 172 +++++++++++
 libgfortran/io/io.h                      |   3 +
 libgfortran/io/read.c                    | 358 +++++++++++++++++++++++
 libgfortran/io/transfer.c                |   8 +
 4 files changed, 541 insertions(+)
 create mode 100644 gcc/testsuite/gfortran.dg/EXformat_4.F90

diff --git a/gcc/testsuite/gfortran.dg/EXformat_4.F90 b/gcc/testsuite/gfortran.dg/EXformat_4.F90
new file mode 100644
index 00000000000..c90857ac3e7
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/EXformat_4.F90
@@ -0,0 +1,172 @@
+! { dg-do run }
+! PR93727 Test EX format READ (read_ex).
+! Covers: hex-significand input (0X...P...), decimal fallback, round-trips,
+! INF/NAN, and all supported real kinds.
+program main
+  implicit none
+  call test04
+  call test08
+  call test10
+  call test16
+
+contains
+
+subroutine test04
+  real(kind=4) :: r4
+  character(kind=1,len=40) :: s
+
+  ! Round-trip: write then read back.
+  s = ' '
+  write(s,'(EX0.0)') -huge(1.0_4)
+  read(s,'(EX40.0)') r4
+  if (r4 /= -huge(1.0_4)) stop 1
+
+  s = ' '
+  write(s,'(EX0.0)') tiny(1.0_4)
+  read(s,'(EX40.0)') r4
+  if (r4 /= tiny(1.0_4)) stop 2
+
+  ! Explicit hex literal input.
+  s = '0X1.8P+0'             ! = 1.5 exactly
+  read(s,'(EX40.0)') r4
+  if (r4 /= 1.5_4) stop 3
+
+  s = '-0X1.P+0'             ! = -1.0
+  read(s,'(EX40.0)') r4
+  if (r4 /= -1.0_4) stop 4
+
+  ! Decimal fallback: no 0X prefix, ordinary E-style input.
+  s = '1.5E0'
+  read(s,'(EX40.0)') r4
+  if (r4 /= 1.5_4) stop 5
+
+  s = '   1.5E2   '
+  read(s,'(EX40.0)') r4
+  if (r4 /= 150.0_4) stop 6
+
+  ! Zero field (blank record).
+  s = '    '
+  read(s,'(EX4.0)') r4
+  if (r4 /= 0.0_4) stop 7
+
+end subroutine test04
+
+
+subroutine test08
+  real(kind=8) :: r8
+  character(kind=1,len=40) :: s
+
+  ! Round-trip.
+  s = ' '
+  write(s,'(EX0.0)') -huge(1.0_8)
+  read(s,'(EX40.0)') r8
+  if (r8 /= -huge(1.0_8)) stop 8
+
+  s = ' '
+  write(s,'(EX0.0)') tiny(1.0_8)
+  read(s,'(EX40.0)') r8
+  if (r8 /= tiny(1.0_8)) stop 9
+
+  ! Explicit hex: IEEE 754 representation of 1/3.
+  s = '0X1.5555555555555P-2'
+  read(s,'(EX40.0)') r8
+  if (r8 /= 1.0_8 / 3.0_8) stop 10
+
+  ! Negative hex.
+  s = '-0X1.8P+0'            ! = -1.5
+  read(s,'(EX40.0)') r8
+  if (r8 /= -1.5_8) stop 11
+
+  ! Decimal fallback.
+  s = '3.14159265358979E0'
+  read(s,'(EX40.0)') r8
+  if (abs(r8 - 3.14159265358979_8) > 2.0_8 * epsilon(r8)) stop 12
+
+  ! Round-trip on an interesting value.
+  s = ' '
+  write(s,'(EX0.0)') -3.14159265358979_8 * 25.0_8
+  read(s,'(EX40.0)') r8
+  if (r8 /= -3.14159265358979_8 * 25.0_8) stop 13
+
+  ! Lowercase hex prefix and negative binary exponent.
+  s = '0x1.8p-1'            ! = 0.75
+  read(s,'(EX40.0)') r8
+  if (r8 /= 0.75_8) stop 14
+
+  ! d-field: no decimal point in decimal input; d=2 places from right.
+  s = '12345   '
+  read(s,'(EX10.2)') r8
+  if (r8 /= 123.45_8) stop 15
+
+  ! INF and NaN input via decimal fallback path.
+  s = 'Inf'
+  read(s,'(EX40.0)') r8
+  if (r8 <= huge(r8)) stop 16
+
+  s = '-Infinity'
+  read(s,'(EX40.0)') r8
+  if (r8 >= -huge(r8)) stop 17
+
+  s = 'NaN'
+  read(s,'(EX40.0)') r8
+  if (r8 == r8) stop 18
+
+end subroutine test08
+
+
+#ifdef __GFC_REAL_10__
+subroutine test10
+  real(kind=10) :: r10
+  character(kind=1,len=40) :: s
+
+  ! Round-trip.
+  s = ' '
+  write(s,'(EX0.0)') -huge(1.0_10)
+  read(s,'(EX40.0)') r10
+  if (r10 /= -huge(1.0_10)) stop 19
+
+  s = ' '
+  write(s,'(EX0.0)') tiny(1.0_10)
+  read(s,'(EX40.0)') r10
+  if (r10 /= tiny(1.0_10)) stop 20
+
+  ! Explicit hex: 1.5 = 0X1.8P+0
+  s = '0X1.8P+0'
+  read(s,'(EX40.0)') r10
+  if (r10 /= 1.5_10) stop 21
+
+end subroutine test10
+#else
+subroutine test10
+end subroutine test10
+#endif
+
+
+#ifdef __GFC_REAL_16__
+subroutine test16
+  real(kind=16) :: r16
+  character(kind=1,len=40) :: s
+
+  ! Round-trip.
+  s = ' '
+  write(s,'(EX0.0)') -huge(1.0_16)
+  read(s,'(EX40.0)') r16
+  if (r16 /= -huge(1.0_16)) stop 22
+
+  s = ' '
+  write(s,'(EX0.0)') tiny(1.0_16)
+  read(s,'(EX40.0)') r16
+  if (r16 /= tiny(1.0_16)) stop 23
+
+  ! Explicit hex: 1.5 = 0X1.8P+0
+  s = '0X1.8P+0'
+  read(s,'(EX40.0)') r16
+  if (r16 /= 1.5_16) stop 24
+
+end subroutine test16
+#else
+subroutine test16
+end subroutine test16
+#endif
+
+end program main
diff --git a/libgfortran/io/io.h b/libgfortran/io/io.h
index 1fd0908859f..d131d9544c4 100644
--- a/libgfortran/io/io.h
+++ b/libgfortran/io/io.h
@@ -887,6 +887,9 @@ internal_proto(read_a);
 extern void read_f (st_parameter_dt *, const fnode *, char *, int);
 internal_proto(read_f);
 
+extern void read_ex (st_parameter_dt *, const fnode *, char *, int);
+internal_proto(read_ex);
+
 extern void read_l (st_parameter_dt *, const fnode *, char *, int);
 internal_proto(read_l);
 
diff --git a/libgfortran/io/read.c b/libgfortran/io/read.c
index aeec725684b..047fff11643 100644
--- a/libgfortran/io/read.c
+++ b/libgfortran/io/read.c
@@ -1525,6 +1525,364 @@ bad_float:
 }
 
 
+/* read_ex()-- Read a floating-point number with EX editing.
+   Per Fortran 2023 13.7.2.3.6 para 3, the input form is the same as for
+   Fw.d editing (13.7.2.3.2).  That includes the hexadecimal-significand
+   form (13.7.2.3.2 para 7): [sign] 0X [hex-significand] P [decimal-exp].
+   Decimal mantissas and IEEE exceptional forms (INF, NAN) are also
+   accepted.  Embedded blanks are not permitted within a hex float; C
+   strtod handles the 0x...p... form natively.  */
+
+void
+read_ex (st_parameter_dt *dtp, const fnode *f, char *dest, int length)
+{
+#define READEX_TMP 64
+  char tmp[READEX_TMP];
+  size_t buf_size = 0;
+  size_t w;
+  int seen_dp, exponent;
+  int exponent_sign;
+  const char *p;
+  char *buffer;
+  char *out;
+  int seen_int_digit;
+  int seen_dec_digit;
+
+  seen_dp = 0;
+  seen_int_digit = 0;
+  seen_dec_digit = 0;
+  exponent_sign = 1;
+  exponent = 0;
+  w = f->u.real.w;
+  buffer = tmp;
+
+  p = read_block_form (dtp, &w);
+  if (p == NULL)
+    return;
+  p = eat_leading_spaces (&w, (char *) p);
+  if (w == 0)
+    goto zero;
+
+  buf_size = w + 11;
+  if (buf_size > READEX_TMP)
+    buffer = xmalloc (buf_size);
+
+  out = buffer;
+
+  /* Optional sign.  */
+  if (*p == '-' || *p == '+')
+    {
+      if (*p == '-')
+	*(out++) = '-';
+      ++p;
+      --w;
+    }
+
+  p = eat_leading_spaces (&w, (char *) p);
+  if (w == 0)
+    {
+      if (buf_size > READEX_TMP)
+	free (buffer);
+      goto zero;
+    }
+
+  /* IEEE exceptional specification: INF or NAN.  */
+  if (unlikely (w >= 3 && (*p == 'i' || *p == 'I' || *p == 'n' || *p == 'N')))
+    {
+      int seen_paren = 0;
+      char *save = out;
+
+      while (w > 0)
+	{
+	  *out = safe_tolower (*p);
+	  switch (*p)
+	    {
+	    case ' ':
+	      if (dtp->u.p.blank_status == BLANK_ZERO)
+		{
+		  *out = '0';
+		  break;
+		}
+	      *out = '\0';
+	      if (seen_paren == 1)
+		goto bad_float;
+	      break;
+	    case '(':
+	      seen_paren++;
+	      *out = '\0';
+	      break;
+	    case ')':
+	      if (seen_paren++ != 1)
+		goto bad_float;
+	      break;
+	    default:
+	      if (!safe_isalnum (*out))
+		goto bad_float;
+	    }
+	  --w;
+	  ++p;
+	  ++out;
+	}
+
+      *out = '\0';
+
+      if (seen_paren != 0 && seen_paren != 2)
+	goto bad_float;
+
+      if ((strcmp (save, "inf") == 0) || (strcmp (save, "infinity") == 0))
+	{
+	  if (seen_paren)
+	    goto bad_float;
+	}
+      else if (strcmp (save, "nan") != 0)
+	goto bad_float;
+
+      convert_infnan (dtp, dest, buffer, length);
+      if (buf_size > READEX_TMP)
+	free (buffer);
+      return;
+    }
+
+  /* Hexadecimal-significand number: 0X or 0x prefix.
+     Embedded blanks are forbidden; we stop at the first blank and let
+     convert_real report an error if the string is malformed.  */
+  if (w >= 2 && *p == '0' && (p[1] == 'x' || p[1] == 'X'))
+    {
+      while (w > 0 && *p != ' ')
+	{
+	  *(out++) = *p++;
+	  --w;
+	}
+      *(out++) = '\0';
+
+      convert_real (dtp, dest, buffer, length);
+      if (buf_size > READEX_TMP)
+	free (buffer);
+      return;
+    }
+
+  /* Decimal floating-point fallback: same rules as Fw.d editing.  */
+  while (w > 0)
+    {
+      switch (*p)
+	{
+	case ',':
+	  if (dtp->u.p.current_unit->decimal_status != DECIMAL_COMMA)
+	    goto bad_float;
+	  if (seen_dp)
+	    goto bad_float;
+	  if (!seen_int_digit)
+	    *(out++) = '0';
+	  *(out++) = '.';
+	  seen_dp = 1;
+	  break;
+
+	case '.':
+	  if (dtp->u.p.current_unit->decimal_status != DECIMAL_POINT)
+	    goto bad_float;
+	  if (seen_dp)
+	    goto bad_float;
+	  if (!seen_int_digit)
+	    *(out++) = '0';
+	  *(out++) = '.';
+	  seen_dp = 1;
+	  break;
+
+	case ' ':
+	  if (dtp->u.p.blank_status == BLANK_ZERO)
+	    {
+	      *(out++) = '0';
+	      goto found_digit;
+	    }
+	  else if (dtp->u.p.blank_status == BLANK_NULL)
+	    break;
+	  else
+	    goto done;
+	  /* Fall through.  */
+	case '0':
+	case '1':
+	case '2':
+	case '3':
+	case '4':
+	case '5':
+	case '6':
+	case '7':
+	case '8':
+	case '9':
+	  *(out++) = *p;
+found_digit:
+	  if (!seen_dp)
+	    seen_int_digit = 1;
+	  else
+	    seen_dec_digit = 1;
+	  break;
+
+	case '-':
+	case '+':
+	  goto exponent;
+
+	case 'e':
+	case 'E':
+	case 'd':
+	case 'D':
+	case 'q':
+	case 'Q':
+	  ++p;
+	  --w;
+	  goto exponent;
+
+	default:
+	  goto bad_float;
+	}
+
+      ++p;
+      --w;
+    }
+
+  /* No exponent seen; apply the scale factor.  */
+  exponent = -dtp->u.p.scale_factor;
+  goto done;
+
+exponent:
+  p = eat_leading_spaces (&w, (char *) p);
+  if (*p == '-' || *p == '+')
+    {
+      if (*p == '-')
+	exponent_sign = -1;
+      ++p;
+      --w;
+    }
+
+  if (w == 0)
+    {
+      if (dtp->common.flags & IOPARM_DT_DEC_EXT)
+	goto done;
+      else
+	goto bad_float;
+    }
+
+  if (dtp->u.p.blank_status == BLANK_UNSPECIFIED)
+    {
+      while (w > 0 && safe_isdigit (*p))
+	{
+	  exponent *= 10;
+	  exponent += *p - '0';
+	  ++p;
+	  --w;
+	}
+      while (w > 0)
+	{
+	  if (*p != ' ')
+	    goto bad_float;
+	  ++p;
+	  --w;
+	}
+    }
+  else
+    {
+      while (w > 0)
+	{
+	  if (*p == ' ')
+	    {
+	      if (dtp->u.p.blank_status == BLANK_ZERO)
+		exponent *= 10;
+	      else
+		assert (dtp->u.p.blank_status == BLANK_NULL);
+	    }
+	  else if (!safe_isdigit (*p))
+	    goto bad_float;
+	  else
+	    {
+	      exponent *= 10;
+	      exponent += *p - '0';
+	    }
+	  ++p;
+	  --w;
+	}
+    }
+
+  exponent *= exponent_sign;
+
+done:
+  if (!seen_dp)
+    exponent -= f->u.real.d;
+
+  if (seen_dp && !seen_dec_digit)
+    *(out++) = '0';
+  else if (!seen_int_digit && !seen_dec_digit)
+    {
+      notify_std (&dtp->common, GFC_STD_LEGACY,
+		  "REAL input of style 'E+NN'");
+      *(out++) = '0';
+    }
+
+  if (exponent != 0)
+    {
+      int dig;
+
+      *(out++) = 'e';
+      if (exponent < 0)
+	{
+	  *(out++) = '-';
+	  exponent = -exponent;
+	}
+
+      if (exponent >= 10000)
+	goto bad_float;
+
+      for (dig = 3; dig >= 0; --dig)
+	{
+	  out[dig] = (char) ('0' + exponent % 10);
+	  exponent /= 10;
+	}
+      out += 4;
+    }
+  *(out++) = '\0';
+
+  convert_real (dtp, dest, buffer, length);
+  if (buf_size > READEX_TMP)
+    free (buffer);
+  return;
+
+zero:
+  switch (length)
+    {
+    case 4:
+      *((GFC_REAL_4 *) dest) = 0.0;
+      break;
+    case 8:
+      *((GFC_REAL_8 *) dest) = 0.0;
+      break;
+#ifdef HAVE_GFC_REAL_10
+    case 10:
+      *((GFC_REAL_10 *) dest) = 0.0;
+      break;
+#endif
+#ifdef HAVE_GFC_REAL_16
+    case 16:
+      *((GFC_REAL_16 *) dest) = 0.0;
+      break;
+#endif
+#ifdef HAVE_GFC_REAL_17
+    case 17:
+      *((GFC_REAL_17 *) dest) = 0.0;
+      break;
+#endif
+    default:
+      internal_error (&dtp->common, "Unsupported real kind during IO");
+    }
+  return;
+
+bad_float:
+  if (buf_size > READEX_TMP)
+    free (buffer);
+  generate_error (&dtp->common, LIBERROR_READ_VALUE,
+		  "Bad value during floating point read");
+  next_record (dtp, 1);
+  return;
+}
+
+
 /* read_x()-- Deal with the X/TR descriptor.  We just read some data
    and never look at it. */
 
diff --git a/libgfortran/io/transfer.c b/libgfortran/io/transfer.c
index 99e90f3c803..49b211ba667 100644
--- a/libgfortran/io/transfer.c
+++ b/libgfortran/io/transfer.c
@@ -1840,6 +1840,14 @@ formatted_transfer_scalar_read (st_parameter_dt *dtp, bt type, void *p, int kind
 	  read_f (dtp, f, p, kind);
 	  break;
 
+	case FMT_EX:
+	  if (n == 0)
+	    goto need_read_data;
+	  if (require_type (dtp, BT_REAL, type, f))
+	    return;
+	  read_ex (dtp, f, p, kind);
+	  break;
+
 	case FMT_F:
 	  if (n == 0)
 	    goto need_read_data;
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.