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

via flang-commits flang-commits at lists.llvm.org
Sat Mar 28 04:46:26 PDT 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-flang-semantics

Author: laoshd

<details>
<summary>Changes</summary>

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.

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


11 Files Affected:

- (modified) flang-rt/include/flang-rt/runtime/format-implementation.h (+9-1) 
- (modified) flang-rt/include/flang-rt/runtime/format.h (+3-1) 
- (modified) flang-rt/lib/runtime/edit-input.cpp (+6) 
- (modified) flang-rt/lib/runtime/edit-output.cpp (+7) 
- (modified) flang-rt/unittests/Runtime/Format.cpp (+4) 
- (modified) flang-rt/unittests/Runtime/NumericalFormatTest.cpp (+63) 
- (modified) flang/include/flang/Common/format.h (+25-8) 
- (modified) flang/include/flang/Parser/format-specification.h (+2-2) 
- (modified) flang/lib/Parser/io-parsers.cpp (+3) 
- (modified) flang/lib/Parser/unparse.cpp (+1) 
- (added) flang/test/Semantics/io19.f90 (+27) 


``````````diff
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

``````````

</details>


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


More information about the flang-commits mailing list