[flang-commits] [flang] 323c3da - [flang] [flang-rt] Implement AT edit descriptor for Fortran 202X with appropriate handling and tests (#189157)

via flang-commits flang-commits at lists.llvm.org
Fri Apr 24 10:47:46 PDT 2026


Author: laoshd
Date: 2026-04-24T12:47:41-05:00
New Revision: 323c3da8dcb81c0fa478fa837954d95b9f39f83e

URL: https://github.com/llvm/llvm-project/commit/323c3da8dcb81c0fa478fa837954d95b9f39f83e
DIFF: https://github.com/llvm/llvm-project/commit/323c3da8dcb81c0fa478fa837954d95b9f39f83e.diff

LOG: [flang] [flang-rt] Implement AT edit descriptor for Fortran 202X with appropriate handling and tests (#189157)

This PR is to add AT edit descriptor, a new feature of Fortran 2023, 5.1
US 10.

AT is like A (character output) but trims trailing blanks before
emitting the string. AT is functionally the same as TRIM intrinsic.

AT is treated as a variant of A during parse. The trimming of the
trailing spaces are implemented the same as TRIM(). There exists a
method, LenTrim, that can be used to do the trimming. However it's local
to a different .cpp file, character.cpp. And the code is so simple that
no need to make extra effort to use LenTrim.

AT is output only and do not accept width.

This PR will fix #181379.

Added: 
    flang/test/Semantics/io19.f90

Modified: 
    flang-rt/include/flang-rt/runtime/format-implementation.h
    flang-rt/include/flang-rt/runtime/format.h
    flang-rt/lib/runtime/edit-input.cpp
    flang-rt/lib/runtime/edit-output.cpp
    flang-rt/unittests/Runtime/Format.cpp
    flang-rt/unittests/Runtime/NumericalFormatTest.cpp
    flang/docs/F202X.md
    flang/include/flang/Common/format.h
    flang/include/flang/Parser/format-specification.h
    flang/lib/Parser/io-parsers.cpp
    flang/lib/Parser/unparse.cpp

Removed: 
    


################################################################################
diff  --git a/flang-rt/include/flang-rt/runtime/format-implementation.h b/flang-rt/include/flang-rt/runtime/format-implementation.h
index 812802b07ac9f..377298545fc2a 100644
--- a/flang-rt/include/flang-rt/runtime/format-implementation.h
+++ b/flang-rt/include/flang-rt/runtime/format-implementation.h
@@ -469,9 +469,8 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
       if (ch != 'P') { // 1PE5.2 - comma not required (C1302)
         CharType peek{Capitalize(PeekNext())};
         if (peek >= 'A' && peek <= 'Z') {
-          if ((ch == 'A' && peek == 'T' /* anticipate F'202X AT editing */) ||
-              ch == 'B' || ch == 'D' || ch == 'E' || ch == 'R' || ch == 'S' ||
-              ch == 'T') {
+          if ((ch == 'A' && peek == 'T') || ch == 'B' || ch == 'D' ||
+              ch == 'E' || ch == 'R' || ch == 'S' || ch == 'T') {
             // Assume a two-letter edit descriptor
             next = peek;
             ++offset_;
@@ -493,6 +492,7 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
               (ch == 'A' || ch == 'I' || ch == 'B' || ch == 'E' || ch == 'D' ||
                   ch == 'O' || ch == 'Z' || ch == 'F' || ch == 'G' ||
                   ch == 'L')) ||
+          (ch == 'A' && next == 'T') ||
           (ch == 'E' && (next == 'N' || next == 'S' || next == 'X')) ||
           (ch == 'D' && next == 'T')) {
         // Data edit descriptor found
@@ -604,13 +604,20 @@ RT_API_ATTRS common::optional<DataEdit> FormatControl<CONTEXT>::GetNextDataEdit(
         edit.variation = next;
         ++offset_;
       }
+    } else if (edit.descriptor == 'A') {
+      if (static_cast<char>(Capitalize(PeekNext())) == 'T') {
+        edit.variation = 'T';
+        ++offset_;
+      }
     }
     // Width is optional for A[w] in the standard and optional
     // for Lw in most compilers.
+    // AT does not accept a width.
     // Intel & (presumably, from bug report) Fujitsu allow
     // a missing 'w' & 'd'/'m' for other edit descriptors -- but not
     // 'd'/'m' with a missing 'w' -- and so interpret "(E)" as "(E0)".
-    if (CharType ch{PeekNext()}; (ch >= '0' && ch <= '9') || ch == '.') {
+    if (CharType ch{PeekNext()};
+        edit.variation != 'T' && ((ch >= '0' && ch <= '9') || ch == '.')) {
       edit.width = GetIntField(context);
       if constexpr (std::is_base_of_v<InputStatementState, CONTEXT>) {
         if (edit.width.value_or(-1) == 0) {

diff  --git a/flang-rt/include/flang-rt/runtime/format.h b/flang-rt/include/flang-rt/runtime/format.h
index c36abaaf15b55..55074e652ce92 100644
--- a/flang-rt/include/flang-rt/runtime/format.h
+++ b/flang-rt/include/flang-rt/runtime/format.h
@@ -59,6 +59,7 @@ struct MutableModes {
 // A single edit descriptor extracted from a FORMAT
 struct DataEdit {
   char descriptor; // capitalized: one of A, I, B, O, Z, F, E(N/S/X), D, G
+                   // AT uses descriptor 'A' with variation 'T'
 
   // Special internal data edit descriptors for list-directed & NAMELIST I/O
   RT_OFFLOAD_VAR_GROUP_BEGIN
@@ -76,7 +77,8 @@ struct DataEdit {
     return IsListDirected() && modes.inNamelist;
   }
 
-  char variation{'\0'}; // N, S, or X for EN, ES, EX; G/l for original G/list
+  char variation{
+      '\0'}; // N, S, or X for EN, ES, EX; T for AT; G/l for original G/list
   common::optional<int> width; // the 'w' field; optional for A
   common::optional<int> digits; // the 'm' or 'd' field
   common::optional<int> expoDigits; // 'Ee' field

diff  --git a/flang-rt/lib/runtime/edit-input.cpp b/flang-rt/lib/runtime/edit-input.cpp
index 9b31ecce82838..23a684d0c0917 100644
--- a/flang-rt/lib/runtime/edit-input.cpp
+++ b/flang-rt/lib/runtime/edit-input.cpp
@@ -1055,6 +1055,12 @@ RT_API_ATTRS bool EditCharacterInput(IoStatementState &io, const DataEdit &edit,
   case DataEdit::ListDirected:
     return EditListDirectedCharacterInput(io, x, lengthChars, edit);
   case 'A':
+    if (edit.variation == 'T') {
+      io.GetIoErrorHandler().SignalError(IostatErrorInFormat,
+          "'AT' edit descriptor may not be used for input");
+      return false;
+    }
+    break;
   case 'G':
     break;
   case 'B':

diff  --git a/flang-rt/lib/runtime/edit-output.cpp b/flang-rt/lib/runtime/edit-output.cpp
index ded76f073aa1a..af2dc0a036687 100644
--- a/flang-rt/lib/runtime/edit-output.cpp
+++ b/flang-rt/lib/runtime/edit-output.cpp
@@ -921,6 +921,14 @@ template <typename CHAR>
 RT_API_ATTRS bool EditCharacterOutput(IoStatementState &io,
     const DataEdit &edit, const CHAR *x, std::size_t length) {
   int len{static_cast<int>(length)};
+  if (edit.descriptor == 'A' && edit.variation == 'T') {
+    // AT edit descriptor: output character value with trailing blanks
+    // removed (F2023 13.7.5.3.1).
+    while (len > 0 && x[len - 1] == static_cast<CHAR>(' ')) {
+      --len;
+    }
+    return EmitEncoded(io, x, len);
+  }
   int width{edit.width.value_or(len)};
   switch (edit.descriptor) {
   case 'A':

diff  --git a/flang-rt/unittests/Runtime/Format.cpp b/flang-rt/unittests/Runtime/Format.cpp
index cd52dc8c54ed5..47b19bf699e05 100644
--- a/flang-rt/unittests/Runtime/Format.cpp
+++ b/flang-rt/unittests/Runtime/Format.cpp
@@ -112,6 +112,10 @@ TEST(FormatTests, FormatStringTraversal) {
               "I4", "E10.1", "E10.1"},
           1},
       {1, "(F)", ResultsTy{"F"}, 1}, // missing 'w'
+      {1, "(AT)", ResultsTy{"AT"}, 1}, // AT edit descriptor
+      {2, "(AT,AT)", ResultsTy{"AT", "AT"}, 1}, // multiple AT
+      {1, "(2AT)", ResultsTy{"2*AT"}, 2}, // AT with repeat
+      {1, "(A10)", ResultsTy{"A10"}, 1}, // A with width (not AT)
   };
 
   for (const auto &[n, format, expect, repeat] : params) {

diff  --git a/flang-rt/unittests/Runtime/NumericalFormatTest.cpp b/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
index bfabf160658e5..6af17e99418b4 100644
--- a/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
+++ b/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
@@ -1018,3 +1018,68 @@ TEST(IOApiTests, ConfusingMinimization) {
   EXPECT_TRUE(CompareFormattedStrings(" 65504. ", got))
       << "expected ' 65504. ', got '" << got << '\''; // not 65500.!
 }
+
+// Test AT edit descriptor (F2023) - trims trailing blanks on output
+TEST(IOApiTests, ATEditDescriptorOutput) {
+  // Helper to test AT formatted output
+  auto testAT = [](const char *format, const std::string &input,
+                    const char *expect) {
+    char buffer[800];
+    auto cookie{IONAME(BeginInternalFormattedOutput)(
+        buffer, sizeof buffer, format, std::strlen(format))};
+    EXPECT_TRUE(IONAME(OutputAscii)(cookie, input.data(), input.size()));
+    auto status{IONAME(EndIoStatement)(cookie)};
+    EXPECT_EQ(status, 0) << "AT test: '" << format << "' failed, status "
+                         << static_cast<int>(status);
+    std::string got{buffer, sizeof buffer};
+    auto lastNonBlank{got.find_last_not_of(" ")};
+    if (lastNonBlank != std::string::npos) {
+      got.resize(lastNonBlank + 1);
+    }
+    EXPECT_TRUE(CompareFormattedStrings(expect, got))
+        << "AT test: format '" << format << "' input '" << input
+        << "' expected '" << expect << "' got '" << got << "'";
+  };
+
+  // AT trims trailing blanks; use a marker character in the format so that the
+  // trimmed width is verified (replacing (AT) with (A) would shift '#' right).
+  testAT("(AT,'#')", "hello     ", "hello#");
+  testAT("(AT,'#')", "hello", "hello#");
+  testAT("(AT,'#')", "  hi  ", "  hi#");
+  testAT("(AT,'#')", "          ", "#"); // all blanks -> empty
+
+  // A (without T) does NOT trim - outputs full length
+  {
+    char buffer[800];
+    const char *format{"(A)"};
+    auto cookie{IONAME(BeginInternalFormattedOutput)(
+        buffer, sizeof buffer, format, std::strlen(format))};
+    EXPECT_TRUE(IONAME(OutputAscii)(cookie, "hi        ", 10));
+    auto status{IONAME(EndIoStatement)(cookie)};
+    EXPECT_EQ(status, 0);
+    std::string got{buffer, sizeof buffer};
+    // A without width outputs all 10 characters including trailing blanks
+    EXPECT_TRUE(CompareFormattedStrings("hi        ", got))
+        << "A test: expected 'hi        ' got '" << got << "'";
+  }
+
+  // Multiple AT descriptors; '#' marker pins the output position so the
+  // trimmed width is verified without manual blank-stripping.
+  {
+    char buffer[800];
+    const char *format{"(AT,1X,AT,'#')"};
+    auto cookie{IONAME(BeginInternalFormattedOutput)(
+        buffer, sizeof buffer, format, std::strlen(format))};
+    EXPECT_TRUE(IONAME(OutputAscii)(cookie, "abc   ", 6));
+    EXPECT_TRUE(IONAME(OutputAscii)(cookie, "def   ", 6));
+    auto status{IONAME(EndIoStatement)(cookie)};
+    EXPECT_EQ(status, 0);
+    std::string got{buffer, sizeof buffer};
+    auto lastNonBlank{got.find_last_not_of(" ")};
+    if (lastNonBlank != std::string::npos) {
+      got.resize(lastNonBlank + 1);
+    }
+    EXPECT_TRUE(CompareFormattedStrings("abc def#", got))
+        << "Multiple AT test: expected 'abc def#' got '" << got << "'";
+  }
+}

diff  --git a/flang/docs/F202X.md b/flang/docs/F202X.md
index 68747a6cf274f..21916e58f4e05 100644
--- a/flang/docs/F202X.md
+++ b/flang/docs/F202X.md
@@ -273,7 +273,7 @@ Implementation status:
   - `LZ` - Processor-dependent (flang treats as LZP)
   - `LZS` - Suppress leading zero (e.g., `.2`)
   - `LZP` - Print leading zero when the field is wide enough (e.g., `0.2`)
-- `AT` edit descriptor: Not yet implemented
+- `AT` edit descriptor: Implemented
 - `LEADING_ZERO=` specifier in OPEN, WRITE and INQUIRE statements: Implemented
 
 #### Intrinsic Module Extensions

diff  --git a/flang/include/flang/Common/format.h b/flang/include/flang/Common/format.h
index 7c9a763d86bae..4348177b5a042 100644
--- a/flang/include/flang/Common/format.h
+++ b/flang/include/flang/Common/format.h
@@ -113,9 +113,9 @@ struct FormatMessage {
 
 // This declaration is logically private to class FormatValidator.
 // It is placed here to work around a clang compilation problem.
-ENUM_CLASS(TokenKind, None, A, B, BN, BZ, D, DC, DP, DT, E, EN, ES, EX, F, G, I,
-    L, LZ, LZP, LZS, O, P, RC, RD, RN, RP, RU, RZ, S, SP, SS, T, TL, TR, X, Z,
-    Colon, Slash,
+ENUM_CLASS(TokenKind, None, A, AT, B, BN, BZ, D, DC, DP, DT, E, EN, ES, EX, F,
+    G, I, L, LZ, LZP, LZS, O, P, RC, RD, RN, RP, RU, RZ, S, SP, SS, T, TL, TR,
+    X, Z, Colon, Slash,
     Backslash, // nonstandard: inhibit newline on output
     Dollar, // nonstandard: inhibit newline on output on terminals
     Star, LParen, RParen, Comma, Point, Sign,
@@ -137,10 +137,10 @@ template <typename CHAR = char> class FormatValidator {
 
 private:
   common::EnumSet<TokenKind, TokenKind_enumSize> itemsWithLeadingInts_{
-      TokenKind::A, TokenKind::B, TokenKind::D, TokenKind::DT, TokenKind::E,
-      TokenKind::EN, TokenKind::ES, TokenKind::EX, TokenKind::F, TokenKind::G,
-      TokenKind::I, TokenKind::L, TokenKind::O, TokenKind::P, TokenKind::X,
-      TokenKind::Z, TokenKind::Slash, TokenKind::LParen};
+      TokenKind::A, TokenKind::AT, TokenKind::B, TokenKind::D, TokenKind::DT,
+      TokenKind::E, TokenKind::EN, TokenKind::ES, TokenKind::EX, TokenKind::F,
+      TokenKind::G, TokenKind::I, TokenKind::L, TokenKind::O, TokenKind::P,
+      TokenKind::X, TokenKind::Z, TokenKind::Slash, TokenKind::LParen};
 
   struct Token {
     Token &set_kind(TokenKind kind) {
@@ -334,7 +334,11 @@ template <typename CHAR> void FormatValidator<CHAR>::NextToken() {
     break;
   }
   case 'A':
-    token_.set_kind(TokenKind::A);
+    if (LookAheadChar() == 'T') {
+      Advance(TokenKind::AT);
+    } else {
+      token_.set_kind(TokenKind::A);
+    }
     break;
   case 'B':
     switch (LookAheadChar()) {
@@ -718,6 +722,21 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
       NextToken();
       check_w();
       break;
+    case TokenKind::AT:
+      // F2023 data-edit-desc -> AT (no w allowed)
+      hasDataEditDesc = true;
+      check_r();
+      NextToken();
+      if (token_.kind() == TokenKind::UnsignedInteger) {
+        ReportError("'AT' edit descriptor does not accept a width value");
+        NextToken();
+        suppressMessageCascade_ =
+            false; // reset to allow the Read check below to also report
+      }
+      if (stmt_ == IoStmtKind::Read) {
+        ReportError("'AT' edit descriptor must not be used for input");
+      }
+      break;
     case TokenKind::B:
     case TokenKind::I:
     case TokenKind::O:

diff  --git a/flang/include/flang/Parser/format-specification.h b/flang/include/flang/Parser/format-specification.h
index 5d37a9c2c0060..63083c1dc0c02 100644
--- a/flang/include/flang/Parser/format-specification.h
+++ b/flang/include/flang/Parser/format-specification.h
@@ -30,13 +30,13 @@ namespace Fortran::format {
 // R1307 data-edit-desc (part 1 of 2) ->
 //         I w [. m] | B w [. m] | O w [. m] | Z w [. m] | F w . d |
 //         E w . d [E e] | EN w . d [E e] | ES w . d [E e] | EX w . d [E e] |
-//         G w [. d [E e]] | L w | A [w] | D w . d
+//         G w [. d [E e]] | L w | A [w] | AT | D w . d
 // R1308 w -> digit-string
 // R1309 m -> digit-string
 // R1310 d -> digit-string
 // R1311 e -> digit-string
 struct IntrinsicTypeDataEditDesc {
-  enum class Kind { I, B, O, Z, F, E, EN, ES, EX, G, L, A, D };
+  enum class Kind { I, B, O, Z, F, E, EN, ES, EX, G, L, A, AT, D };
   IntrinsicTypeDataEditDesc() = delete;
   IntrinsicTypeDataEditDesc(IntrinsicTypeDataEditDesc &&) = default;
   IntrinsicTypeDataEditDesc &operator=(IntrinsicTypeDataEditDesc &&) = default;

diff  --git a/flang/lib/Parser/io-parsers.cpp b/flang/lib/Parser/io-parsers.cpp
index 2d046f613b86d..60b3c4434896c 100644
--- a/flang/lib/Parser/io-parsers.cpp
+++ b/flang/lib/Parser/io-parsers.cpp
@@ -597,7 +597,7 @@ constexpr auto mandatoryDigits{construct<std::optional<int>>("." >> width)};
 // R1307 data-edit-desc ->
 //         I w [. m] | B w [. m] | O w [. m] | Z w [. m] | F w . d |
 //         E w . d [E e] | EN w . d [E e] | ES w . d [E e] | EX w . d [E e] |
-//         G w [. d [E e]] | L w | A [w] | D w . d |
+//         G w [. d [E e]] | L w | A [w] | AT | D w . d |
 //         DT [char-literal-constant] [( v-list )]
 // (part 1 of 2)
 TYPE_PARSER(construct<format::IntrinsicTypeDataEditDesc>(
@@ -623,6 +623,9 @@ TYPE_PARSER(construct<format::IntrinsicTypeDataEditDesc>(
         "G " >> pure(format::IntrinsicTypeDataEditDesc::Kind::G) ||
             "L " >> pure(format::IntrinsicTypeDataEditDesc::Kind::L),
         mandatoryWidth, noInt, noInt) ||
+    construct<format::IntrinsicTypeDataEditDesc>(
+        "A " >> ("T " >> pure(format::IntrinsicTypeDataEditDesc::Kind::AT)),
+        noInt, noInt, noInt) ||
     construct<format::IntrinsicTypeDataEditDesc>(
         "A " >> pure(format::IntrinsicTypeDataEditDesc::Kind::A), maybe(width),
         noInt, noInt) ||

diff  --git a/flang/lib/Parser/unparse.cpp b/flang/lib/Parser/unparse.cpp
index 46a3b38697c30..99dbff44821cc 100644
--- a/flang/lib/Parser/unparse.cpp
+++ b/flang/lib/Parser/unparse.cpp
@@ -1498,6 +1498,7 @@ class UnparseVisitor {
       FMT(G);
       FMT(L);
       FMT(A);
+      FMT(AT);
       FMT(D);
 #undef FMT
     }

diff  --git a/flang/test/Semantics/io19.f90 b/flang/test/Semantics/io19.f90
new file mode 100644
index 0000000000000..e62fcfce57924
--- /dev/null
+++ b/flang/test/Semantics/io19.f90
@@ -0,0 +1,48 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Test AT edit descriptor (Fortran 2023)
+
+  character(10) :: str
+  character(kind=2,len=10) :: str2
+  character(kind=4,len=10) :: str4
+
+  ! Valid: AT with WRITE
+  write(*, '(AT)') 'hello'
+  write(*, '(AT)') str
+  write(*, '(2AT)') 'abc', 'def'
+
+  ! Valid: AT with non-default character kinds
+  write(*, '(AT)') str2
+  write(*, '(AT)') str4
+  write(*, '(2AT)') str2, str4
+
+  ! Valid: AT in FORMAT statement for WRITE
+1 format(AT)
+  write(*,1) 'hello'
+
+  ! Error: AT must not be used for input
+  !ERROR: 'AT' edit descriptor must not be used for input
+  read(*, '(AT)') str
+
+  ! Error: AT must not be used for input with non-default kinds
+  !ERROR: 'AT' edit descriptor must not be used for input
+  read(*, '(AT)') str2
+  !ERROR: 'AT' edit descriptor must not be used for input
+  read(*, '(AT)') str4
+
+  ! AT does not accept a width
+  !ERROR: 'AT' edit descriptor does not accept a width value
+  write(*, '(AT10)') str
+  !ERROR: 'AT' edit descriptor does not accept a width value
+  !ERROR: Unexpected '.' in format expression
+  write(*, '(AT10.2)') str
+
+  ! AT with width on input produces two independent errors.
+  !ERROR: 'AT' edit descriptor does not accept a width value
+  !ERROR: 'AT' edit descriptor must not be used for input
+  read(*, '(AT10)') str
+
+  ! FORMAT statements are standalone; the compiler cannot know if they will
+  ! be used with READ or WRITE, so no compile-time error is expected here.
+2 format(AT)
+  read(*,2) str
+end


        


More information about the flang-commits mailing list