[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