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

via llvm-commits llvm-commits at lists.llvm.org
Mon Mar 30 12:50:57 PDT 2026


https://github.com/laoshd updated https://github.com/llvm/llvm-project/pull/189157

>From 875f90bc8dd8327901a6448551d1a8c169b4fbfd Mon Sep 17 00:00:00 2001
From: Shandong Lao <shandong.lao at hpe.com>
Date: Sat, 28 Mar 2026 05:54:56 -0500
Subject: [PATCH 1/2] [flang] [flang-rt] Implement AT edit descriptor for
 Fortran 202X with appropriate handling and tests. Fixes #181379.

---
 .../flang-rt/runtime/format-implementation.h  | 10 ++-
 flang-rt/include/flang-rt/runtime/format.h    |  4 +-
 flang-rt/lib/runtime/edit-input.cpp           |  6 ++
 flang-rt/lib/runtime/edit-output.cpp          |  7 +++
 flang-rt/unittests/Runtime/Format.cpp         |  4 ++
 .../unittests/Runtime/NumericalFormatTest.cpp | 63 +++++++++++++++++++
 flang/include/flang/Common/format.h           | 33 +++++++---
 .../flang/Parser/format-specification.h       |  4 +-
 flang/lib/Parser/io-parsers.cpp               |  3 +
 flang/lib/Parser/unparse.cpp                  |  1 +
 flang/test/Semantics/io19.f90                 | 27 ++++++++
 11 files changed, 150 insertions(+), 12 deletions(-)
 create mode 100644 flang/test/Semantics/io19.f90

diff --git a/flang-rt/include/flang-rt/runtime/format-implementation.h b/flang-rt/include/flang-rt/runtime/format-implementation.h
index 812802b07ac9f..fc1e26162ac97 100644
--- a/flang-rt/include/flang-rt/runtime/format-implementation.h
+++ b/flang-rt/include/flang-rt/runtime/format-implementation.h
@@ -493,6 +493,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 +605,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..b4cde5ca60080 100644
--- a/flang-rt/lib/runtime/edit-output.cpp
+++ b/flang-rt/lib/runtime/edit-output.cpp
@@ -921,6 +921,13 @@ 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: trim trailing blanks (F202X TRIM semantics)
+    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..c221a310bbd4d 100644
--- a/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
+++ b/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
@@ -1018,3 +1018,66 @@ TEST(IOApiTests, ConfusingMinimization) {
   EXPECT_TRUE(CompareFormattedStrings(" 65504. ", got))
       << "expected ' 65504. ', got '" << got << '\''; // not 65500.!
 }
+
+// Test AT edit descriptor (F202X) - trims trailing blanks on output
+TEST(IOApiTests, ATEditDescriptorOutput) {
+  // Helper to test AT formatted output
+  auto testAT = [](const char *format, const char *input, std::size_t inputLen,
+                    const char *expect) {
+    char buffer[800];
+    auto cookie{IONAME(BeginInternalFormattedOutput)(
+        buffer, sizeof buffer, format, std::strlen(format))};
+    EXPECT_TRUE(IONAME(OutputAscii)(cookie, input, inputLen));
+    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
+  testAT("(AT)", "hello     ", 10, "hello");
+  testAT("(AT)", "hello", 5, "hello");
+  testAT("(AT)", "  hi  ", 6, "  hi");
+  testAT("(AT)", "          ", 10, ""); // 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
+  {
+    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/include/flang/Common/format.h b/flang/include/flang/Common/format.h
index 7c9a763d86bae..17cfe96e5a96a 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,19 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
       NextToken();
       check_w();
       break;
+    case TokenKind::AT:
+      // F202X 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();
+      }
+      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..5218d072edd3a 100644
--- a/flang/lib/Parser/io-parsers.cpp
+++ b/flang/lib/Parser/io-parsers.cpp
@@ -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 c31eac0b3ff68..6afb25269fff1 100644
--- a/flang/lib/Parser/unparse.cpp
+++ b/flang/lib/Parser/unparse.cpp
@@ -1487,6 +1487,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..0d86c10cf6fa3
--- /dev/null
+++ b/flang/test/Semantics/io19.f90
@@ -0,0 +1,27 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1 -pedantic
+! Test AT edit descriptor (Fortran 202X)
+
+  character(10) :: str
+
+  ! Valid: AT with WRITE
+  write(*, '(AT)') 'hello'
+  write(*, '(AT)') str
+  write(*, '(2AT)') 'abc', 'def'
+
+  ! 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
+
+  ! AT does not accept a width
+  !ERROR: 'AT' edit descriptor does not accept a width value
+  write(*, '(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

>From 0dc3689567dbd52271845792d2c12fb37244569c Mon Sep 17 00:00:00 2001
From: Shandong Lao <shandong.lao at hpe.com>
Date: Sat, 28 Mar 2026 17:05:56 -0500
Subject: [PATCH 2/2] Update doc for AT implementation.

---
 flang/docs/F202X.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/flang/docs/F202X.md b/flang/docs/F202X.md
index 462f6040f9cea..c756343a0c159 100644
--- a/flang/docs/F202X.md
+++ b/flang/docs/F202X.md
@@ -267,7 +267,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



More information about the llvm-commits mailing list