Skip to content

[flang][flang-rt] Implement F202X leading-zero control edit descriptors LZ, LZS, and LZP for formatted output (F, E, D, and G editing)#183500

Open
laoshd wants to merge 14 commits intollvm:mainfrom
laoshd:users/laoshd/I-178494
Open

[flang][flang-rt] Implement F202X leading-zero control edit descriptors LZ, LZS, and LZP for formatted output (F, E, D, and G editing)#183500
laoshd wants to merge 14 commits intollvm:mainfrom
laoshd:users/laoshd/I-178494

Conversation

@laoshd
Copy link
Contributor

@laoshd laoshd commented Feb 26, 2026

LZ: processor-dependent (default, flang prints leading zero); LZS: suppress the optional leading zero before the decimal point; LZP: print the optional leading zero before the decimal point. Changes span the source parser, compile-time format validator, runtime format processing, and runtime output formatting. Includes semantic test (io16.f90) and documentation updates.

…ero control edit descriptors LZ, LZS, and LZP for formatted output (F, E, D, and G editing): LZ: processor-dependent (default, flang prints leading zero); LZS: suppress the optional leading zero before the decimal point; LZP: print the optional leading zero before the decimal point. Changes span the source parser, compile-time format validator, runtime format processing, and runtime output formatting. Includes semantic test (io16.f90) and documentation updates.
@github-actions
Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot llvmbot added flang Flang issues not falling into any other category flang:semantics flang:parser flang-rt labels Feb 26, 2026
@llvmbot
Copy link
Member

llvmbot commented Feb 26, 2026

@llvm/pr-subscribers-flang-semantics

Author: None (laoshd)

Changes

…control edit descriptors LZ, LZS, and LZP for formatted output (F, E, D, and G editing): LZ: processor-dependent (default, flang prints leading zero); LZS: suppress the optional leading zero before the decimal point; LZP: print the optional leading zero before the decimal point. Changes span the source parser, compile-time format validator, runtime format processing, and runtime output formatting. Includes semantic test (io16.f90) and documentation updates.


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

10 Files Affected:

  • (modified) flang-rt/include/flang-rt/runtime/format-implementation.h (+27-2)
  • (modified) flang-rt/include/flang-rt/runtime/format.h (+7)
  • (modified) flang-rt/lib/runtime/edit-output.cpp (+38-9)
  • (modified) flang/docs/F202X.md (+9)
  • (modified) flang/docs/FortranStandardsSupport.md (+1-1)
  • (modified) flang/include/flang/Common/format.h (+40-5)
  • (modified) flang/include/flang/Parser/format-specification.h (+3)
  • (modified) flang/lib/Parser/io-parsers.cpp (+8-1)
  • (modified) flang/lib/Parser/unparse.cpp (+3)
  • (added) flang/test/Semantics/io16.f90 (+99)
diff --git a/flang-rt/include/flang-rt/runtime/format-implementation.h b/flang-rt/include/flang-rt/runtime/format-implementation.h
index d510adbb5ba46..8b341def2b3ce 100644
--- a/flang-rt/include/flang-rt/runtime/format-implementation.h
+++ b/flang-rt/include/flang-rt/runtime/format-implementation.h
@@ -193,7 +193,7 @@ static RT_API_ATTRS bool AbsoluteTabbing(CONTEXT &context, int n) {
 
 template <typename CONTEXT>
 static RT_API_ATTRS void HandleControl(
-    CONTEXT &context, char ch, char next, int n) {
+    CONTEXT &context, char ch, char next, int n, char next2 = '\0') {
   MutableModes &modes{context.mutableModes()};
   switch (ch) {
   case 'B':
@@ -251,6 +251,21 @@ static RT_API_ATTRS void HandleControl(
       return;
     }
     break;
+  case 'L':
+    if (next == 'Z') {
+      if (next2 == 'S') {
+        // LZS - suppress leading zeros
+        modes.leadingZero = MutableModes::LeadingZeroMode::Suppress;
+      } else if (next2 == 'P') {
+        // LZP - print leading zero
+        modes.leadingZero = MutableModes::LeadingZeroMode::Print;
+      } else {
+        // LZ - processor-dependent (default behavior)
+        modes.leadingZero = MutableModes::LeadingZeroMode::Processor;
+      }
+      return;
+    }
+    break;
   case 'S':
     if (next == 'P') {
       modes.editingFlags |= signPlus;
@@ -455,6 +470,7 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
     } else if (ch >= 'A' && ch <= 'Z') {
       int start{offset_ - 1};
       CharType next{'\0'};
+      CharType next2{'\0'};
       if (ch != 'P') { // 1PE5.2 - comma not required (C1302)
         CharType peek{Capitalize(PeekNext())};
         if (peek >= 'A' && peek <= 'Z') {
@@ -464,6 +480,15 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
             // Assume a two-letter edit descriptor
             next = peek;
             ++offset_;
+          } else if (ch == 'L' && peek == 'Z') {
+            // LZ, LZS, or LZP control edit descriptor
+            next = peek;
+            ++offset_;
+            CharType peek2{Capitalize(PeekNext())};
+            if (peek2 == 'S' || peek2 == 'P') {
+              next2 = peek2;
+              ++offset_;
+            }
           } else {
             // extension: assume a comma between 'ch' and 'peek'
           }
@@ -484,7 +509,7 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
           repeat = GetIntField(context);
         }
         HandleControl(context, static_cast<char>(ch), static_cast<char>(next),
-            repeat ? *repeat : 1);
+            repeat ? *repeat : 1, static_cast<char>(next2));
       }
     } else if (ch == '/') {
       context.AdvanceRecord(repeat && *repeat > 0 ? *repeat : 1);
diff --git a/flang-rt/include/flang-rt/runtime/format.h b/flang-rt/include/flang-rt/runtime/format.h
index 79a7dd713b1a1..787772935ce8c 100644
--- a/flang-rt/include/flang-rt/runtime/format.h
+++ b/flang-rt/include/flang-rt/runtime/format.h
@@ -44,6 +44,12 @@ struct MutableModes {
     return editingFlags & decimalComma ? char32_t{','} : char32_t{'.'};
   }
 
+  enum class LeadingZeroMode : std::uint8_t {
+    Processor, // LZ: processor-dependent (default)
+    Suppress, // LZS: suppress optional leading zero
+    Print, // LZP: print optional leading zero
+  };
+
   std::uint8_t editingFlags{0}; // BN, DP, SS
   enum decimal::FortranRounding round{
       executionEnvironment
@@ -53,6 +59,7 @@ struct MutableModes {
   short scale{0}; // kP
   bool inNamelist{false}; // skip ! comments
   bool nonAdvancing{false}; // ADVANCE='NO', or $ or \ in FORMAT
+  LeadingZeroMode leadingZero{LeadingZeroMode::Processor}; // LZ/LZS/LZP
 };
 
 // A single edit descriptor extracted from a FORMAT
diff --git a/flang-rt/lib/runtime/edit-output.cpp b/flang-rt/lib/runtime/edit-output.cpp
index 73dba35ff08d9..d0aa8638102a9 100644
--- a/flang-rt/lib/runtime/edit-output.cpp
+++ b/flang-rt/lib/runtime/edit-output.cpp
@@ -411,10 +411,26 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditEorDOutput(
     if (totalLength > width || !exponent) {
       return EmitRepeated(io_, '*', width);
     }
-    if (totalLength < width && digitsBeforePoint == 0 &&
-        zeroesBeforePoint == 0) {
-      zeroesBeforePoint = 1;
-      ++totalLength;
+    if (digitsBeforePoint == 0 && zeroesBeforePoint == 0 && scale <= 0) {
+      // Optional leading zero position (F202X leading zero control).
+      // When scale > 0 (kP with k > 0), digits are moved before the decimal
+      // point, so the leading zero position is not optional -- skip this.
+      // Value has no digits before the decimal point: "0.xxxE+yy" vs ".xxxE+yy"
+      switch (edit.modes.leadingZero) {
+      case MutableModes::LeadingZeroMode::Print:
+        // LZP: always print the optional leading zero
+        zeroesBeforePoint = 1;
+        ++totalLength;
+        break;
+      case MutableModes::LeadingZeroMode::Suppress:
+        // LZS: never print the optional leading zero
+        break;
+      case MutableModes::LeadingZeroMode::Processor:
+        // LZ: processor-defined; flang chooses to print it
+        zeroesBeforePoint = 1;
+        ++totalLength;
+        break;
+      }
     }
     if (totalLength < width && editWidth == 0) {
       width = totalLength;
@@ -544,7 +560,24 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
     if (digitsBeforePoint + zeroesBeforePoint + zeroesAfterPoint +
             digitsAfterPoint + trailingZeroes ==
         0) {
-      zeroesBeforePoint = 1; // "." -> "0."
+      zeroesBeforePoint = 1; // "."--> "0." (bare decimal point)
+    } else if (digitsBeforePoint == 0 && zeroesBeforePoint == 0 &&
+        expo <= 0) {
+      // Optional leading zero position (F202X leading zero control).
+      // Value magnitude < 1: "0.xxx" vs ".xxx"
+      switch (edit.modes.leadingZero) {
+      case MutableModes::LeadingZeroMode::Print:
+        // LZP: always print the optional leading zero
+        zeroesBeforePoint = 1;
+        break;
+      case MutableModes::LeadingZeroMode::Suppress:
+        // LZS: never print the optional leading zero
+        break;
+      case MutableModes::LeadingZeroMode::Processor:
+        // LZ: processor-defined; flang chooses to print it
+        zeroesBeforePoint = 1;
+        break;
+      }
     }
     int totalLength{signLength + digitsBeforePoint + zeroesBeforePoint +
         1 /*'.'*/ + zeroesAfterPoint + digitsAfterPoint + trailingZeroes +
@@ -553,10 +586,6 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
     if (totalLength > width) {
       return EmitRepeated(io_, '*', width);
     }
-    if (totalLength < width && digitsBeforePoint + zeroesBeforePoint == 0) {
-      zeroesBeforePoint = 1;
-      ++totalLength;
-    }
     return EmitPrefix(edit, totalLength, width) &&
         EmitAscii(io_, convertedStr, signLength + digitsBeforePoint) &&
         EmitRepeated(io_, '0', zeroesBeforePoint) &&
diff --git a/flang/docs/F202X.md b/flang/docs/F202X.md
index d1940a1858db1..6a303981cf264 100644
--- a/flang/docs/F202X.md
+++ b/flang/docs/F202X.md
@@ -261,6 +261,15 @@ The `AT` edit descriptor automatically trims character output.  The `LZP`,
 `LZS`, and `LZ` control edit descriptors and `LEADING_ZERO=` specifier provide a
 means for controlling the output of leading zero digits.
 
+Implementation status:
+- `LZ`, `LZS`, `LZP` control edit descriptors, affect only F, E, D, and G
+  editing of an output statement: Implemented
+  - `LZ` - Processor-dependent, default (e.g., `0.2`)
+  - `LZS` - Suppress leading zeros (e.g., `.2`)
+  - `LZP` - Print leading zero (e.g. `0.2`)
+- `AT` edit descriptor: Not yet implemented
+- `LEADING_ZERO=specifier` in OPEN statement: Not yet implemented
+
 #### Intrinsic Module Extensions
 
 Addressing some issues and omissions in intrinsic modules:
diff --git a/flang/docs/FortranStandardsSupport.md b/flang/docs/FortranStandardsSupport.md
index f57956cd6d6b8..02e4653280f12 100644
--- a/flang/docs/FortranStandardsSupport.md
+++ b/flang/docs/FortranStandardsSupport.md
@@ -48,7 +48,7 @@ status of all important Fortran 2023 features. The table entries are based on th
 | Extensions for c_f_pointer intrinsic                       | Y      | |
 | Procedures for converting between fortran and c strings    | N      | |
 | The at edit descriptor                                     | N      | |
-| Control over leading zeros in output of real values        | N      | |
+| Control over leading zeros in output of real values        | P      | LZ/LZS/LZP edit descriptors implemented; LEADING_ZERO=specifier not yet implemented     |
 | Extensions for Namelist                                    | N      | |
 | Allow an object of a type with a coarray ultimate component to be an array or allocatable | N | |
 | Put with Notify                                            | N      | |
diff --git a/flang/include/flang/Common/format.h b/flang/include/flang/Common/format.h
index 1e64acb823616..60bcc8e81ee38 100644
--- a/flang/include/flang/Common/format.h
+++ b/flang/include/flang/Common/format.h
@@ -114,7 +114,7 @@ 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, O, P, RC, RD, RN, RP, RU, RZ, S, SP, SS, T, TL, TR, X, Z, Colon, Slash,
+    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,
@@ -219,7 +219,7 @@ template <typename CHAR = char> class FormatValidator {
   std::int64_t knrValue_{-1}; // -1 ==> not present
   std::int64_t scaleFactorValue_{}; // signed k in kP
   std::int64_t wValue_{-1};
-  char argString_[3]{}; // 1-2 character msg arg; usually edit descriptor name
+  char argString_[4]{}; // 1-3 character msg arg; usually edit descriptor name
   bool formatHasErrors_{false};
   bool unterminatedFormatError_{false};
   bool suppressMessageCascade_{false};
@@ -390,7 +390,25 @@ template <typename CHAR> void FormatValidator<CHAR>::NextToken() {
     token_.set_kind(TokenKind::I);
     break;
   case 'L':
-    token_.set_kind(TokenKind::L);
+    switch (LookAheadChar()) {
+    case 'Z':
+      // Advance past 'Z', then look ahead for 'S' or 'P'
+      Advance(TokenKind::LZ);
+      switch (LookAheadChar()) {
+      case 'S':
+        Advance(TokenKind::LZS);
+        break;
+      case 'P':
+        Advance(TokenKind::LZP);
+        break;
+      default:
+        break;
+      }
+      break;
+    default:
+      token_.set_kind(TokenKind::L);
+      break;
+    }
     break;
   case 'O':
     token_.set_kind(TokenKind::O);
@@ -674,9 +692,22 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
       ReportError("Unexpected '%s' in format expression", signToken);
     }
     // Default message argument.
-    // Alphabetic edit descriptor names are one or two characters in length.
+    // Alphabetic edit descriptor names are one to three characters in length.
     argString_[0] = toupper(format_[token_.offset()]);
-    argString_[1] = token_.length() > 1 ? toupper(*cursor_) : 0;
+    if (token_.length() > 2) {
+      // Three-character descriptor names (e.g., LZP, LZS).
+      // token_.offset() has the first character and *cursor_ has the last;
+      // find the middle character by scanning past any blanks.
+      const CHAR *mid{format_ + token_.offset() + 1};
+      while (mid < cursor_ && IsWhite(*mid)) {
+        ++mid;
+      }
+      argString_[1] = toupper(*mid);
+      argString_[2] = toupper(*cursor_);
+    } else {
+      argString_[1] = token_.length() > 1 ? toupper(*cursor_) : 0;
+      argString_[2] = 0;
+    }
     // Process one format edit descriptor or do format list management.
     switch (token_.kind()) {
     case TokenKind::A:
@@ -794,6 +825,9 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
     case TokenKind::BZ:
     case TokenKind::DC:
     case TokenKind::DP:
+    case TokenKind::LZ:
+    case TokenKind::LZS:
+    case TokenKind::LZP:
     case TokenKind::RC:
     case TokenKind::RD:
     case TokenKind::RN:
@@ -807,6 +841,7 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
       // R1318 blank-interp-edit-desc -> BN | BZ
       // R1319 round-edit-desc -> RU | RD | RZ | RN | RC | RP
       // R1320 decimal-edit-desc -> DC | DP
+      // F202X leading-zero-edit-desc -> LZ | LZS | LZP
       check_r(false);
       NextToken();
       break;
diff --git a/flang/include/flang/Parser/format-specification.h b/flang/include/flang/Parser/format-specification.h
index 28c8affd7bde0..5d37a9c2c0060 100644
--- a/flang/include/flang/Parser/format-specification.h
+++ b/flang/include/flang/Parser/format-specification.h
@@ -95,6 +95,9 @@ struct ControlEditDesc {
     RP,
     DC,
     DP,
+    LZ, // F202X: processor-dependent leading zero, default
+    LZS, // F202X: suppress leading zeros
+    LZP, // F202X: print leading zero
     Dollar, // extension: inhibit newline on output
     Backslash, // ditto, but only on terminals
   };
diff --git a/flang/lib/Parser/io-parsers.cpp b/flang/lib/Parser/io-parsers.cpp
index cb3e68a05c94d..9da8c4f01d7dc 100644
--- a/flang/lib/Parser/io-parsers.cpp
+++ b/flang/lib/Parser/io-parsers.cpp
@@ -629,7 +629,8 @@ TYPE_PARSER(construct<format::IntrinsicTypeDataEditDesc>(
                     "X " >> pure(format::IntrinsicTypeDataEditDesc::Kind::EX) ||
                     pure(format::IntrinsicTypeDataEditDesc::Kind::E)) ||
             "G " >> pure(format::IntrinsicTypeDataEditDesc::Kind::G) ||
-            "L " >> pure(format::IntrinsicTypeDataEditDesc::Kind::L),
+            ("L "_tok / !letter /* don't occlude LZ, LZS, & LZP */) >>
+                pure(format::IntrinsicTypeDataEditDesc::Kind::L),
         noInt, noInt, noInt)))
 
 // R1307 data-edit-desc (part 2 of 2)
@@ -677,6 +678,12 @@ TYPE_PARSER(construct<format::ControlEditDesc>(
                          pure(format::ControlEditDesc::Kind::BN)) ||
                 "Z " >> construct<format::ControlEditDesc>(
                             pure(format::ControlEditDesc::Kind::BZ))) ||
+    "L " >> ("Z " >> ("S " >> construct<format::ControlEditDesc>(
+                                  pure(format::ControlEditDesc::Kind::LZS)) ||
+                          "P " >> construct<format::ControlEditDesc>(
+                                      pure(format::ControlEditDesc::Kind::LZP)) ||
+                          construct<format::ControlEditDesc>(
+                              pure(format::ControlEditDesc::Kind::LZ)))) ||
     "R " >> ("U " >> construct<format::ControlEditDesc>(
                          pure(format::ControlEditDesc::Kind::RU)) ||
                 "D " >> construct<format::ControlEditDesc>(
diff --git a/flang/lib/Parser/unparse.cpp b/flang/lib/Parser/unparse.cpp
index 3d8ea9f703b2f..8ad1f9b8ff618 100644
--- a/flang/lib/Parser/unparse.cpp
+++ b/flang/lib/Parser/unparse.cpp
@@ -1549,6 +1549,9 @@ class UnparseVisitor {
       FMT(RP);
       FMT(DC);
       FMT(DP);
+      FMT(LZ);
+      FMT(LZS);
+      FMT(LZP);
 #undef FMT
     case format::ControlEditDesc::Kind::Dollar:
       Put('$');
diff --git a/flang/test/Semantics/io16.f90 b/flang/test/Semantics/io16.f90
new file mode 100644
index 0000000000000..559d98f0d5ceb
--- /dev/null
+++ b/flang/test/Semantics/io16.f90
@@ -0,0 +1,99 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+
+! F202X leading-zero control edit descriptors: LZ, LZS, LZP
+
+  ! Valid uses of LZ, LZP, LZS in FORMAT statements
+1001 format(LZ, F10.3)
+1002 format(LZP, F10.3)
+1003 format(LZS, F10.3)
+1004 format(LZ, E10.3)
+1005 format(LZP, E10.3)
+1006 format(LZS, E10.3)
+1007 format(LZS, D10.3)
+1008 format(LZ, G10.3)
+
+  ! Valid uses with blanks inside keywords (Fortran ignores blanks)
+1009 format(L Z, F10.3)
+1010 format(L Z P, F10.3)
+1011 format(L Z S, F10.3)
+
+  ! Combining with other control edit descriptors
+1012 format(LZP, DC, F10.3)
+1013 format(BN, LZS, F10.3)
+1014 format(LZ, SS, RZ, F10.3)
+
+  ! Multiple groups
+1015 format(LZP, 3F10.3, LZS, 2E12.4)
+
+  ! C1302 : multiple edit descriptors without ',' separation; no errors
+1016 format(LZF10.3)
+1017 format(LZPF10.3)
+1018 format(LZSF10.3)
+1019 format(LZE10.3)
+1020 format(LZPE10.3)
+1021 format(LZSD10.3)
+1022 format(LZG10.3)
+1023 format(LZPDCF10.3)
+1024 format(BNLZSF10.3)
+1025 format(LZPF10.3LZSF10.3)
+1026 format(LZP3F10.3LZS2E12.4)
+
+  ! In WRITE format strings
+  write(*, '(LZ, F10.3)') 0.5
+  write(*, '(LZP, F10.3)') 0.5
+  write(*, '(LZS, F10.3)') 0.5
+  write(*, '(LZP,E10.3)') 0.5
+  write(*, '(LZS,D10.3)') 0.5
+
+  ! C1302 : WRITE format strings without ',' separation; no errors
+  write(*, '(LZF10.3)') 0.5
+  write(*, '(LZPF10.3)') 0.5
+  write(*, '(LZSF10.3)') 0.5
+  write(*, '(LZPE10.3)') 0.5
+  write(*, '(LZP3F10.3LZS2E12.4)') 0.5, 0.5, 0.5, 0.5, 0.5
+
+  ! FMT= specifier with comma-separated descriptors
+  write(*, fmt='(LZ, F10.3)') 0.5
+  write(*, fmt='(LZP, F10.3)') 0.5
+  write(*, fmt='(LZS, F10.3)') 0.5
+  write(*, fmt='(LZP, E10.3)') 0.5
+  write(*, fmt='(LZS, D10.3)') 0.5
+  write(*, fmt='(LZP, DC, F10.3)') 0.5
+  write(*, fmt='(BN, LZS, F10.3)') 0.5
+
+  ! FMT= specifier without ',' separation; no errors
+  write(*, fmt='(LZF10.3)') 0.5
+  write(*, fmt='(LZPF10.3)') 0.5
+  write(*, fmt='(LZSF10.3)') 0.5
+  write(*, fmt='(LZPE10.3)') 0.5
+  write(*, fmt='(LZP3F10.3LZS2E12.4)') 0.5, 0.5, 0.5, 0.5, 0.5
+
+  ! FMT= specifier with FORMAT label reference
+  write(*, fmt=1001) 0.5
+  write(*, fmt=1002) 0.5
+  write(*, fmt=1017) 0.5
+
+  ! LZ/LZP/LZS coexisting with abbreviated L (no width) data edit descriptor
+  write(*, '(LZP, F10.3, L)') 0.5, .true.
+  write(*, '(LZS, F10.3, L)') 0.5, .true.
+
+  ! Error: repeat specifier before LZ/LZP/LZS in WRITE format strings
+  !ERROR: Repeat specifier before 'LZ' edit descriptor
+  write(*, '(3LZ, F10.3)') 0.5
+
+  !ERROR: Repeat specifier before 'LZP' edit descriptor
+  write(*, '(2LZP, F10.3)') 0.5
+
+  !ERROR: Repeat specifier before 'LZS' edit descriptor
+  write(*, '(2LZS, F10.3)') 0.5
+
+  ! Error: repeat specifier before LZ/LZP/LZS in FORMAT statements
+  !ERROR: Repeat specifier before 'LZ' edit descriptor
+2001 format(3LZ, F10.3)
+
+  !ERROR: Repeat specifier before 'LZP' edit descriptor
+2002 format(2LZP, F10.3)
+
+  !ERROR: Repeat specifier before 'LZS' edit descriptor
+2003 format(2LZS, F10.3)
+end

@llvmbot
Copy link
Member

llvmbot commented Feb 26, 2026

@llvm/pr-subscribers-flang-parser

Author: None (laoshd)

Changes

…control edit descriptors LZ, LZS, and LZP for formatted output (F, E, D, and G editing): LZ: processor-dependent (default, flang prints leading zero); LZS: suppress the optional leading zero before the decimal point; LZP: print the optional leading zero before the decimal point. Changes span the source parser, compile-time format validator, runtime format processing, and runtime output formatting. Includes semantic test (io16.f90) and documentation updates.


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

10 Files Affected:

  • (modified) flang-rt/include/flang-rt/runtime/format-implementation.h (+27-2)
  • (modified) flang-rt/include/flang-rt/runtime/format.h (+7)
  • (modified) flang-rt/lib/runtime/edit-output.cpp (+38-9)
  • (modified) flang/docs/F202X.md (+9)
  • (modified) flang/docs/FortranStandardsSupport.md (+1-1)
  • (modified) flang/include/flang/Common/format.h (+40-5)
  • (modified) flang/include/flang/Parser/format-specification.h (+3)
  • (modified) flang/lib/Parser/io-parsers.cpp (+8-1)
  • (modified) flang/lib/Parser/unparse.cpp (+3)
  • (added) flang/test/Semantics/io16.f90 (+99)
diff --git a/flang-rt/include/flang-rt/runtime/format-implementation.h b/flang-rt/include/flang-rt/runtime/format-implementation.h
index d510adbb5ba46..8b341def2b3ce 100644
--- a/flang-rt/include/flang-rt/runtime/format-implementation.h
+++ b/flang-rt/include/flang-rt/runtime/format-implementation.h
@@ -193,7 +193,7 @@ static RT_API_ATTRS bool AbsoluteTabbing(CONTEXT &context, int n) {
 
 template <typename CONTEXT>
 static RT_API_ATTRS void HandleControl(
-    CONTEXT &context, char ch, char next, int n) {
+    CONTEXT &context, char ch, char next, int n, char next2 = '\0') {
   MutableModes &modes{context.mutableModes()};
   switch (ch) {
   case 'B':
@@ -251,6 +251,21 @@ static RT_API_ATTRS void HandleControl(
       return;
     }
     break;
+  case 'L':
+    if (next == 'Z') {
+      if (next2 == 'S') {
+        // LZS - suppress leading zeros
+        modes.leadingZero = MutableModes::LeadingZeroMode::Suppress;
+      } else if (next2 == 'P') {
+        // LZP - print leading zero
+        modes.leadingZero = MutableModes::LeadingZeroMode::Print;
+      } else {
+        // LZ - processor-dependent (default behavior)
+        modes.leadingZero = MutableModes::LeadingZeroMode::Processor;
+      }
+      return;
+    }
+    break;
   case 'S':
     if (next == 'P') {
       modes.editingFlags |= signPlus;
@@ -455,6 +470,7 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
     } else if (ch >= 'A' && ch <= 'Z') {
       int start{offset_ - 1};
       CharType next{'\0'};
+      CharType next2{'\0'};
       if (ch != 'P') { // 1PE5.2 - comma not required (C1302)
         CharType peek{Capitalize(PeekNext())};
         if (peek >= 'A' && peek <= 'Z') {
@@ -464,6 +480,15 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
             // Assume a two-letter edit descriptor
             next = peek;
             ++offset_;
+          } else if (ch == 'L' && peek == 'Z') {
+            // LZ, LZS, or LZP control edit descriptor
+            next = peek;
+            ++offset_;
+            CharType peek2{Capitalize(PeekNext())};
+            if (peek2 == 'S' || peek2 == 'P') {
+              next2 = peek2;
+              ++offset_;
+            }
           } else {
             // extension: assume a comma between 'ch' and 'peek'
           }
@@ -484,7 +509,7 @@ RT_API_ATTRS int FormatControl<CONTEXT>::CueUpNextDataEdit(
           repeat = GetIntField(context);
         }
         HandleControl(context, static_cast<char>(ch), static_cast<char>(next),
-            repeat ? *repeat : 1);
+            repeat ? *repeat : 1, static_cast<char>(next2));
       }
     } else if (ch == '/') {
       context.AdvanceRecord(repeat && *repeat > 0 ? *repeat : 1);
diff --git a/flang-rt/include/flang-rt/runtime/format.h b/flang-rt/include/flang-rt/runtime/format.h
index 79a7dd713b1a1..787772935ce8c 100644
--- a/flang-rt/include/flang-rt/runtime/format.h
+++ b/flang-rt/include/flang-rt/runtime/format.h
@@ -44,6 +44,12 @@ struct MutableModes {
     return editingFlags & decimalComma ? char32_t{','} : char32_t{'.'};
   }
 
+  enum class LeadingZeroMode : std::uint8_t {
+    Processor, // LZ: processor-dependent (default)
+    Suppress, // LZS: suppress optional leading zero
+    Print, // LZP: print optional leading zero
+  };
+
   std::uint8_t editingFlags{0}; // BN, DP, SS
   enum decimal::FortranRounding round{
       executionEnvironment
@@ -53,6 +59,7 @@ struct MutableModes {
   short scale{0}; // kP
   bool inNamelist{false}; // skip ! comments
   bool nonAdvancing{false}; // ADVANCE='NO', or $ or \ in FORMAT
+  LeadingZeroMode leadingZero{LeadingZeroMode::Processor}; // LZ/LZS/LZP
 };
 
 // A single edit descriptor extracted from a FORMAT
diff --git a/flang-rt/lib/runtime/edit-output.cpp b/flang-rt/lib/runtime/edit-output.cpp
index 73dba35ff08d9..d0aa8638102a9 100644
--- a/flang-rt/lib/runtime/edit-output.cpp
+++ b/flang-rt/lib/runtime/edit-output.cpp
@@ -411,10 +411,26 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditEorDOutput(
     if (totalLength > width || !exponent) {
       return EmitRepeated(io_, '*', width);
     }
-    if (totalLength < width && digitsBeforePoint == 0 &&
-        zeroesBeforePoint == 0) {
-      zeroesBeforePoint = 1;
-      ++totalLength;
+    if (digitsBeforePoint == 0 && zeroesBeforePoint == 0 && scale <= 0) {
+      // Optional leading zero position (F202X leading zero control).
+      // When scale > 0 (kP with k > 0), digits are moved before the decimal
+      // point, so the leading zero position is not optional -- skip this.
+      // Value has no digits before the decimal point: "0.xxxE+yy" vs ".xxxE+yy"
+      switch (edit.modes.leadingZero) {
+      case MutableModes::LeadingZeroMode::Print:
+        // LZP: always print the optional leading zero
+        zeroesBeforePoint = 1;
+        ++totalLength;
+        break;
+      case MutableModes::LeadingZeroMode::Suppress:
+        // LZS: never print the optional leading zero
+        break;
+      case MutableModes::LeadingZeroMode::Processor:
+        // LZ: processor-defined; flang chooses to print it
+        zeroesBeforePoint = 1;
+        ++totalLength;
+        break;
+      }
     }
     if (totalLength < width && editWidth == 0) {
       width = totalLength;
@@ -544,7 +560,24 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
     if (digitsBeforePoint + zeroesBeforePoint + zeroesAfterPoint +
             digitsAfterPoint + trailingZeroes ==
         0) {
-      zeroesBeforePoint = 1; // "." -> "0."
+      zeroesBeforePoint = 1; // "."--> "0." (bare decimal point)
+    } else if (digitsBeforePoint == 0 && zeroesBeforePoint == 0 &&
+        expo <= 0) {
+      // Optional leading zero position (F202X leading zero control).
+      // Value magnitude < 1: "0.xxx" vs ".xxx"
+      switch (edit.modes.leadingZero) {
+      case MutableModes::LeadingZeroMode::Print:
+        // LZP: always print the optional leading zero
+        zeroesBeforePoint = 1;
+        break;
+      case MutableModes::LeadingZeroMode::Suppress:
+        // LZS: never print the optional leading zero
+        break;
+      case MutableModes::LeadingZeroMode::Processor:
+        // LZ: processor-defined; flang chooses to print it
+        zeroesBeforePoint = 1;
+        break;
+      }
     }
     int totalLength{signLength + digitsBeforePoint + zeroesBeforePoint +
         1 /*'.'*/ + zeroesAfterPoint + digitsAfterPoint + trailingZeroes +
@@ -553,10 +586,6 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
     if (totalLength > width) {
       return EmitRepeated(io_, '*', width);
     }
-    if (totalLength < width && digitsBeforePoint + zeroesBeforePoint == 0) {
-      zeroesBeforePoint = 1;
-      ++totalLength;
-    }
     return EmitPrefix(edit, totalLength, width) &&
         EmitAscii(io_, convertedStr, signLength + digitsBeforePoint) &&
         EmitRepeated(io_, '0', zeroesBeforePoint) &&
diff --git a/flang/docs/F202X.md b/flang/docs/F202X.md
index d1940a1858db1..6a303981cf264 100644
--- a/flang/docs/F202X.md
+++ b/flang/docs/F202X.md
@@ -261,6 +261,15 @@ The `AT` edit descriptor automatically trims character output.  The `LZP`,
 `LZS`, and `LZ` control edit descriptors and `LEADING_ZERO=` specifier provide a
 means for controlling the output of leading zero digits.
 
+Implementation status:
+- `LZ`, `LZS`, `LZP` control edit descriptors, affect only F, E, D, and G
+  editing of an output statement: Implemented
+  - `LZ` - Processor-dependent, default (e.g., `0.2`)
+  - `LZS` - Suppress leading zeros (e.g., `.2`)
+  - `LZP` - Print leading zero (e.g. `0.2`)
+- `AT` edit descriptor: Not yet implemented
+- `LEADING_ZERO=specifier` in OPEN statement: Not yet implemented
+
 #### Intrinsic Module Extensions
 
 Addressing some issues and omissions in intrinsic modules:
diff --git a/flang/docs/FortranStandardsSupport.md b/flang/docs/FortranStandardsSupport.md
index f57956cd6d6b8..02e4653280f12 100644
--- a/flang/docs/FortranStandardsSupport.md
+++ b/flang/docs/FortranStandardsSupport.md
@@ -48,7 +48,7 @@ status of all important Fortran 2023 features. The table entries are based on th
 | Extensions for c_f_pointer intrinsic                       | Y      | |
 | Procedures for converting between fortran and c strings    | N      | |
 | The at edit descriptor                                     | N      | |
-| Control over leading zeros in output of real values        | N      | |
+| Control over leading zeros in output of real values        | P      | LZ/LZS/LZP edit descriptors implemented; LEADING_ZERO=specifier not yet implemented     |
 | Extensions for Namelist                                    | N      | |
 | Allow an object of a type with a coarray ultimate component to be an array or allocatable | N | |
 | Put with Notify                                            | N      | |
diff --git a/flang/include/flang/Common/format.h b/flang/include/flang/Common/format.h
index 1e64acb823616..60bcc8e81ee38 100644
--- a/flang/include/flang/Common/format.h
+++ b/flang/include/flang/Common/format.h
@@ -114,7 +114,7 @@ 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, O, P, RC, RD, RN, RP, RU, RZ, S, SP, SS, T, TL, TR, X, Z, Colon, Slash,
+    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,
@@ -219,7 +219,7 @@ template <typename CHAR = char> class FormatValidator {
   std::int64_t knrValue_{-1}; // -1 ==> not present
   std::int64_t scaleFactorValue_{}; // signed k in kP
   std::int64_t wValue_{-1};
-  char argString_[3]{}; // 1-2 character msg arg; usually edit descriptor name
+  char argString_[4]{}; // 1-3 character msg arg; usually edit descriptor name
   bool formatHasErrors_{false};
   bool unterminatedFormatError_{false};
   bool suppressMessageCascade_{false};
@@ -390,7 +390,25 @@ template <typename CHAR> void FormatValidator<CHAR>::NextToken() {
     token_.set_kind(TokenKind::I);
     break;
   case 'L':
-    token_.set_kind(TokenKind::L);
+    switch (LookAheadChar()) {
+    case 'Z':
+      // Advance past 'Z', then look ahead for 'S' or 'P'
+      Advance(TokenKind::LZ);
+      switch (LookAheadChar()) {
+      case 'S':
+        Advance(TokenKind::LZS);
+        break;
+      case 'P':
+        Advance(TokenKind::LZP);
+        break;
+      default:
+        break;
+      }
+      break;
+    default:
+      token_.set_kind(TokenKind::L);
+      break;
+    }
     break;
   case 'O':
     token_.set_kind(TokenKind::O);
@@ -674,9 +692,22 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
       ReportError("Unexpected '%s' in format expression", signToken);
     }
     // Default message argument.
-    // Alphabetic edit descriptor names are one or two characters in length.
+    // Alphabetic edit descriptor names are one to three characters in length.
     argString_[0] = toupper(format_[token_.offset()]);
-    argString_[1] = token_.length() > 1 ? toupper(*cursor_) : 0;
+    if (token_.length() > 2) {
+      // Three-character descriptor names (e.g., LZP, LZS).
+      // token_.offset() has the first character and *cursor_ has the last;
+      // find the middle character by scanning past any blanks.
+      const CHAR *mid{format_ + token_.offset() + 1};
+      while (mid < cursor_ && IsWhite(*mid)) {
+        ++mid;
+      }
+      argString_[1] = toupper(*mid);
+      argString_[2] = toupper(*cursor_);
+    } else {
+      argString_[1] = token_.length() > 1 ? toupper(*cursor_) : 0;
+      argString_[2] = 0;
+    }
     // Process one format edit descriptor or do format list management.
     switch (token_.kind()) {
     case TokenKind::A:
@@ -794,6 +825,9 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
     case TokenKind::BZ:
     case TokenKind::DC:
     case TokenKind::DP:
+    case TokenKind::LZ:
+    case TokenKind::LZS:
+    case TokenKind::LZP:
     case TokenKind::RC:
     case TokenKind::RD:
     case TokenKind::RN:
@@ -807,6 +841,7 @@ template <typename CHAR> bool FormatValidator<CHAR>::Check() {
       // R1318 blank-interp-edit-desc -> BN | BZ
       // R1319 round-edit-desc -> RU | RD | RZ | RN | RC | RP
       // R1320 decimal-edit-desc -> DC | DP
+      // F202X leading-zero-edit-desc -> LZ | LZS | LZP
       check_r(false);
       NextToken();
       break;
diff --git a/flang/include/flang/Parser/format-specification.h b/flang/include/flang/Parser/format-specification.h
index 28c8affd7bde0..5d37a9c2c0060 100644
--- a/flang/include/flang/Parser/format-specification.h
+++ b/flang/include/flang/Parser/format-specification.h
@@ -95,6 +95,9 @@ struct ControlEditDesc {
     RP,
     DC,
     DP,
+    LZ, // F202X: processor-dependent leading zero, default
+    LZS, // F202X: suppress leading zeros
+    LZP, // F202X: print leading zero
     Dollar, // extension: inhibit newline on output
     Backslash, // ditto, but only on terminals
   };
diff --git a/flang/lib/Parser/io-parsers.cpp b/flang/lib/Parser/io-parsers.cpp
index cb3e68a05c94d..9da8c4f01d7dc 100644
--- a/flang/lib/Parser/io-parsers.cpp
+++ b/flang/lib/Parser/io-parsers.cpp
@@ -629,7 +629,8 @@ TYPE_PARSER(construct<format::IntrinsicTypeDataEditDesc>(
                     "X " >> pure(format::IntrinsicTypeDataEditDesc::Kind::EX) ||
                     pure(format::IntrinsicTypeDataEditDesc::Kind::E)) ||
             "G " >> pure(format::IntrinsicTypeDataEditDesc::Kind::G) ||
-            "L " >> pure(format::IntrinsicTypeDataEditDesc::Kind::L),
+            ("L "_tok / !letter /* don't occlude LZ, LZS, & LZP */) >>
+                pure(format::IntrinsicTypeDataEditDesc::Kind::L),
         noInt, noInt, noInt)))
 
 // R1307 data-edit-desc (part 2 of 2)
@@ -677,6 +678,12 @@ TYPE_PARSER(construct<format::ControlEditDesc>(
                          pure(format::ControlEditDesc::Kind::BN)) ||
                 "Z " >> construct<format::ControlEditDesc>(
                             pure(format::ControlEditDesc::Kind::BZ))) ||
+    "L " >> ("Z " >> ("S " >> construct<format::ControlEditDesc>(
+                                  pure(format::ControlEditDesc::Kind::LZS)) ||
+                          "P " >> construct<format::ControlEditDesc>(
+                                      pure(format::ControlEditDesc::Kind::LZP)) ||
+                          construct<format::ControlEditDesc>(
+                              pure(format::ControlEditDesc::Kind::LZ)))) ||
     "R " >> ("U " >> construct<format::ControlEditDesc>(
                          pure(format::ControlEditDesc::Kind::RU)) ||
                 "D " >> construct<format::ControlEditDesc>(
diff --git a/flang/lib/Parser/unparse.cpp b/flang/lib/Parser/unparse.cpp
index 3d8ea9f703b2f..8ad1f9b8ff618 100644
--- a/flang/lib/Parser/unparse.cpp
+++ b/flang/lib/Parser/unparse.cpp
@@ -1549,6 +1549,9 @@ class UnparseVisitor {
       FMT(RP);
       FMT(DC);
       FMT(DP);
+      FMT(LZ);
+      FMT(LZS);
+      FMT(LZP);
 #undef FMT
     case format::ControlEditDesc::Kind::Dollar:
       Put('$');
diff --git a/flang/test/Semantics/io16.f90 b/flang/test/Semantics/io16.f90
new file mode 100644
index 0000000000000..559d98f0d5ceb
--- /dev/null
+++ b/flang/test/Semantics/io16.f90
@@ -0,0 +1,99 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+
+! F202X leading-zero control edit descriptors: LZ, LZS, LZP
+
+  ! Valid uses of LZ, LZP, LZS in FORMAT statements
+1001 format(LZ, F10.3)
+1002 format(LZP, F10.3)
+1003 format(LZS, F10.3)
+1004 format(LZ, E10.3)
+1005 format(LZP, E10.3)
+1006 format(LZS, E10.3)
+1007 format(LZS, D10.3)
+1008 format(LZ, G10.3)
+
+  ! Valid uses with blanks inside keywords (Fortran ignores blanks)
+1009 format(L Z, F10.3)
+1010 format(L Z P, F10.3)
+1011 format(L Z S, F10.3)
+
+  ! Combining with other control edit descriptors
+1012 format(LZP, DC, F10.3)
+1013 format(BN, LZS, F10.3)
+1014 format(LZ, SS, RZ, F10.3)
+
+  ! Multiple groups
+1015 format(LZP, 3F10.3, LZS, 2E12.4)
+
+  ! C1302 : multiple edit descriptors without ',' separation; no errors
+1016 format(LZF10.3)
+1017 format(LZPF10.3)
+1018 format(LZSF10.3)
+1019 format(LZE10.3)
+1020 format(LZPE10.3)
+1021 format(LZSD10.3)
+1022 format(LZG10.3)
+1023 format(LZPDCF10.3)
+1024 format(BNLZSF10.3)
+1025 format(LZPF10.3LZSF10.3)
+1026 format(LZP3F10.3LZS2E12.4)
+
+  ! In WRITE format strings
+  write(*, '(LZ, F10.3)') 0.5
+  write(*, '(LZP, F10.3)') 0.5
+  write(*, '(LZS, F10.3)') 0.5
+  write(*, '(LZP,E10.3)') 0.5
+  write(*, '(LZS,D10.3)') 0.5
+
+  ! C1302 : WRITE format strings without ',' separation; no errors
+  write(*, '(LZF10.3)') 0.5
+  write(*, '(LZPF10.3)') 0.5
+  write(*, '(LZSF10.3)') 0.5
+  write(*, '(LZPE10.3)') 0.5
+  write(*, '(LZP3F10.3LZS2E12.4)') 0.5, 0.5, 0.5, 0.5, 0.5
+
+  ! FMT= specifier with comma-separated descriptors
+  write(*, fmt='(LZ, F10.3)') 0.5
+  write(*, fmt='(LZP, F10.3)') 0.5
+  write(*, fmt='(LZS, F10.3)') 0.5
+  write(*, fmt='(LZP, E10.3)') 0.5
+  write(*, fmt='(LZS, D10.3)') 0.5
+  write(*, fmt='(LZP, DC, F10.3)') 0.5
+  write(*, fmt='(BN, LZS, F10.3)') 0.5
+
+  ! FMT= specifier without ',' separation; no errors
+  write(*, fmt='(LZF10.3)') 0.5
+  write(*, fmt='(LZPF10.3)') 0.5
+  write(*, fmt='(LZSF10.3)') 0.5
+  write(*, fmt='(LZPE10.3)') 0.5
+  write(*, fmt='(LZP3F10.3LZS2E12.4)') 0.5, 0.5, 0.5, 0.5, 0.5
+
+  ! FMT= specifier with FORMAT label reference
+  write(*, fmt=1001) 0.5
+  write(*, fmt=1002) 0.5
+  write(*, fmt=1017) 0.5
+
+  ! LZ/LZP/LZS coexisting with abbreviated L (no width) data edit descriptor
+  write(*, '(LZP, F10.3, L)') 0.5, .true.
+  write(*, '(LZS, F10.3, L)') 0.5, .true.
+
+  ! Error: repeat specifier before LZ/LZP/LZS in WRITE format strings
+  !ERROR: Repeat specifier before 'LZ' edit descriptor
+  write(*, '(3LZ, F10.3)') 0.5
+
+  !ERROR: Repeat specifier before 'LZP' edit descriptor
+  write(*, '(2LZP, F10.3)') 0.5
+
+  !ERROR: Repeat specifier before 'LZS' edit descriptor
+  write(*, '(2LZS, F10.3)') 0.5
+
+  ! Error: repeat specifier before LZ/LZP/LZS in FORMAT statements
+  !ERROR: Repeat specifier before 'LZ' edit descriptor
+2001 format(3LZ, F10.3)
+
+  !ERROR: Repeat specifier before 'LZP' edit descriptor
+2002 format(2LZP, F10.3)
+
+  !ERROR: Repeat specifier before 'LZS' edit descriptor
+2003 format(2LZS, F10.3)
+end

@eugeneepshteyn
Copy link
Contributor

@laoshd , could you please also add flang-rt tests for the new edit descriptors? Also, it may be a good idea to also add some execution tests to llvm-test-suite.

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

✅ With the latest revision this PR passed the C/C++ code formatter.

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

🪟 Windows x64 Test Results

  • 4032 tests passed
  • 245 tests skipped

✅ The build succeeded and all tests passed.

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

🐧 Linux x64 Test Results

  • 4372 tests passed
  • 201 tests skipped

✅ The build succeeded and all tests passed.

…zero control edit descriptors in FORMAT statements, flang/test/Semantics/io17.f90. Previously added io16.f90 was used.
@laoshd
Copy link
Contributor Author

laoshd commented Feb 26, 2026

🪟 Windows x64 Test Results

  • 3980 tests passed
  • 245 tests skipped
  • 1 test failed

Failed Tests

(click on a test name to see its output)

Flang

Flang.Semantics/io16.f90

Exit Code: 1

Command Output (stdout):
--
# RUN: at line 1
"C:\Python312\python.exe" C:\_work\llvm-project\llvm-project\flang\test\Semantics/test_errors.py C:\_work\llvm-project\llvm-project\flang\test\Semantics\io16.f90 c:\_work\llvm-project\llvm-project\build\bin\flang.exe -fc1
# executed command: 'C:\Python312\python.exe' 'C:\_work\llvm-project\llvm-project\flang\test\Semantics/test_errors.py' 'C:\_work\llvm-project\llvm-project\flang\test\Semantics\io16.f90' 'c:\_work\llvm-project\llvm-project\build\bin\flang.exe' -fc1
# .---command stdout------------
# | --- 
# | +++ 
# | @@ -1,3 +1,6 @@
# | 
# | actual at 92: expected ':'
# | actual at 95: expected ':'
# | actual at 98: expected ':'
# | expect at 82: Repeat specifier before 'LZ' edit descriptor
# | expect at 85: Repeat specifier before 'LZP' edit descriptor
# | expect at 88: Repeat specifier before 'LZS' edit descriptor
# | expect at 92: Repeat specifier before 'LZ' edit descriptor
# | expect at 95: Repeat specifier before 'LZP' edit descriptor
# | expect at 98: Repeat specifier before 'LZS' edit descriptor
# | 
# | FAIL
# `-----------------------------
# error: command failed with exit status: 1

--

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

This test failure is a from an existing issue: #183581

@clementval
Copy link
Contributor

Can you update the title of your PR so it fits in?

@laoshd laoshd changed the title [flang][flang-rt][F202X][Issue #178494] Implement F202X leading-zero … [llvm/llvm-project][flang][flang-rt][F202X][Issue #178494] Implement F202X leading-zero … Feb 27, 2026
… and docs format; Add flang-rt test, LeadingZeroTest.cpp.
@laoshd
Copy link
Contributor Author

laoshd commented Feb 27, 2026

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:

git-clang-format --diff origin/main HEAD --extensions cpp,h -- flang-rt/include/flang-rt/runtime/format-implementation.h flang-rt/include/flang-rt/runtime/format.h flang-rt/lib/runtime/edit-output.cpp flang/include/flang/Common/format.h flang/include/flang/Parser/format-specification.h flang/lib/Parser/io-parsers.cpp flang/lib/Parser/unparse.cpp --diff_from_common_commit

⚠️ The reproduction instructions above might return results for more than one PR in a stack if you are using a stacked PR workflow. You can limit the results by changing origin/main to the base branch/commit you want to compare against. ⚠️

View the diff from clang-format here.

diff --git a/flang-rt/lib/runtime/edit-output.cpp b/flang-rt/lib/runtime/edit-output.cpp
index d0aa86381..fbfcef6e7 100644
--- a/flang-rt/lib/runtime/edit-output.cpp
+++ b/flang-rt/lib/runtime/edit-output.cpp
@@ -561,8 +561,7 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
             digitsAfterPoint + trailingZeroes ==
         0) {
       zeroesBeforePoint = 1; // "."--> "0." (bare decimal point)
-    } else if (digitsBeforePoint == 0 && zeroesBeforePoint == 0 &&
-        expo <= 0) {
+    } else if (digitsBeforePoint == 0 && zeroesBeforePoint == 0 && expo <= 0) {
       // Optional leading zero position (F202X leading zero control).
       // Value magnitude < 1: "0.xxx" vs ".xxx"
       switch (edit.modes.leadingZero) {
diff --git a/flang/include/flang/Common/format.h b/flang/include/flang/Common/format.h
index 60bcc8e81..8597c709d 100644
--- a/flang/include/flang/Common/format.h
+++ b/flang/include/flang/Common/format.h
@@ -114,7 +114,8 @@ 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,
+    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,
diff --git a/flang/lib/Parser/io-parsers.cpp b/flang/lib/Parser/io-parsers.cpp
index 9da8c4f01..93aa4e9d2 100644
--- a/flang/lib/Parser/io-parsers.cpp
+++ b/flang/lib/Parser/io-parsers.cpp
@@ -680,10 +680,10 @@ TYPE_PARSER(construct<format::ControlEditDesc>(
                             pure(format::ControlEditDesc::Kind::BZ))) ||
     "L " >> ("Z " >> ("S " >> construct<format::ControlEditDesc>(
                                   pure(format::ControlEditDesc::Kind::LZS)) ||
-                          "P " >> construct<format::ControlEditDesc>(
-                                      pure(format::ControlEditDesc::Kind::LZP)) ||
-                          construct<format::ControlEditDesc>(
-                              pure(format::ControlEditDesc::Kind::LZ)))) ||
+                         "P " >> construct<format::ControlEditDesc>(pure(
+                                     format::ControlEditDesc::Kind::LZP)) ||
+                         construct<format::ControlEditDesc>(
+                             pure(format::ControlEditDesc::Kind::LZ)))) ||
     "R " >> ("U " >> construct<format::ControlEditDesc>(
                          pure(format::ControlEditDesc::Kind::RU)) ||
                 "D " >> construct<format::ControlEditDesc>(

Updated.

@laoshd
Copy link
Contributor Author

laoshd commented Feb 27, 2026

@laoshd , could you please also add flang-rt tests for the new edit descriptors? Also, it may be a good idea to also add some execution tests to llvm-test-suite.

A flang-rt test has been added. I'll add execution test to llvm-test-suite in a separated PR. Will that work? Please let me know if the execution has also to be added in this PR. Thanks.

@laoshd
Copy link
Contributor Author

laoshd commented Feb 27, 2026

Can you update the title of your PR so it fits in?

I have updated the title. Please advise if I still missed something. Thanks.

@tarunprabhu
Copy link
Contributor

I have updated the title. Please advise if I still missed something. Thanks.

Please read the instructions regarding commit messages in the LLVM Developer Policy. Your title does not adhere to the guidelines. You can also look at other commit messages in the flang subdirectory to get a sense for what this should look like.

Please address the failures in the pre-commit CI below.

@clementval
Copy link
Contributor

Can you update the title of your PR so it fits in?

I have updated the title. Please advise if I still missed something. Thanks.

I think it is worse than before. You can just have [flang][flang-rt] as prefix and a short title that follow that will fit in the allowed space. The rest can go in the description.

@laoshd laoshd changed the title [llvm/llvm-project][flang][flang-rt][F202X][Issue #178494] Implement F202X leading-zero … [flang][flang-rt] Implement F202X leading-zero control edit descriptors LZ, LZS, and LZP for formatted output (F, E, D, and G editing) Feb 27, 2026
@laoshd
Copy link
Contributor Author

laoshd commented Feb 27, 2026

🪟 Windows x64 Test Results

  • 3980 tests passed
  • 245 tests skipped
  • 1 test failed

Failed Tests

(click on a test name to see its output)

Flang

Flang.Semantics/io16.f90

Exit Code: 1

Command Output (stdout):
--
# RUN: at line 1
"C:\Python312\python.exe" C:\_work\llvm-project\llvm-project\flang\test\Semantics/test_errors.py C:\_work\llvm-project\llvm-project\flang\test\Semantics\io16.f90 c:\_work\llvm-project\llvm-project\build\bin\flang.exe -fc1
# executed command: 'C:\Python312\python.exe' 'C:\_work\llvm-project\llvm-project\flang\test\Semantics/test_errors.py' 'C:\_work\llvm-project\llvm-project\flang\test\Semantics\io16.f90' 'c:\_work\llvm-project\llvm-project\build\bin\flang.exe' -fc1
# .---command stdout------------
# | --- 
# | +++ 
# | @@ -1,3 +1,6 @@
# | 
# | actual at 92: expected ':'
# | actual at 95: expected ':'
# | actual at 98: expected ':'
# | expect at 82: Repeat specifier before 'LZ' edit descriptor
# | expect at 85: Repeat specifier before 'LZP' edit descriptor
# | expect at 88: Repeat specifier before 'LZS' edit descriptor
# | expect at 92: Repeat specifier before 'LZ' edit descriptor
# | expect at 95: Repeat specifier before 'LZP' edit descriptor
# | expect at 98: Repeat specifier before 'LZS' edit descriptor
# | 
# | FAIL
# `-----------------------------
# error: command failed with exit status: 1

--

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

This test failure is a from an existing issue: #183581

This test failure is because issue #183581 (#183581).

@laoshd
Copy link
Contributor Author

laoshd commented Feb 27, 2026

🐧 Linux x64 Test Results

  • 4029 tests passed
  • 201 tests skipped
  • 1 test failed

Failed Tests

(click on a test name to see its output)

Flang

Flang.Semantics/io17.f90

Exit Code: 1

Command Output (stdout):
--
# RUN: at line 1
"/usr/bin/python3" /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/test_errors.py /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/io17.f90 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/bin/flang -fc1
# executed command: /usr/bin/python3 /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/test_errors.py /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/io17.f90 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/bin/flang -fc1
# .---command stdout------------
# | --- 
# | +++ 
# | @@ -1,3 +1,6 @@
# | 
# | actual at 92: expected ':'
# | actual at 95: expected ':'
# | actual at 98: expected ':'
# | expect at 82: Repeat specifier before 'LZ' edit descriptor
# | expect at 85: Repeat specifier before 'LZP' edit descriptor
# | expect at 88: Repeat specifier before 'LZS' edit descriptor
# | expect at 92: Repeat specifier before 'LZ' edit descriptor
# | expect at 95: Repeat specifier before 'LZP' edit descriptor
# | expect at 98: Repeat specifier before 'LZS' edit descriptor
# | 
# | FAIL
# `-----------------------------
# error: command failed with exit status: 1

--

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

This test failure is because issue #183581 (#183581).

@laoshd
Copy link
Contributor Author

laoshd commented Feb 27, 2026

Can you update the title of your PR so it fits in?

I have updated the title. Please advise if I still missed something. Thanks.

I think it is worse than before. You can just have [flang][flang-rt] as prefix and a short title that follow that will fit in the allowed space. The rest can go in the description.

Updated.

eugeneepshteyn pushed a commit that referenced this pull request Mar 5, 2026
Fix the problem described in issue #178494. It will cover the failures
with S, SP, SS, BN, BZ, LZ, LZP, LZS, etc. It will resolve the test
failures in PR #183500.
@laoshd
Copy link
Contributor Author

laoshd commented Mar 6, 2026

🪟 Windows x64 Test Results

  • 3980 tests passed
  • 245 tests skipped
  • 1 test failed

Failed Tests

(click on a test name to see its output)

Flang

Flang.Semantics/io16.f90

Exit Code: 1

Command Output (stdout):
--
# RUN: at line 1
"C:\Python312\python.exe" C:\_work\llvm-project\llvm-project\flang\test\Semantics/test_errors.py C:\_work\llvm-project\llvm-project\flang\test\Semantics\io16.f90 c:\_work\llvm-project\llvm-project\build\bin\flang.exe -fc1
# executed command: 'C:\Python312\python.exe' 'C:\_work\llvm-project\llvm-project\flang\test\Semantics/test_errors.py' 'C:\_work\llvm-project\llvm-project\flang\test\Semantics\io16.f90' 'c:\_work\llvm-project\llvm-project\build\bin\flang.exe' -fc1
# .---command stdout------------
# | --- 
# | +++ 
# | @@ -1,3 +1,6 @@
# | 
# | actual at 92: expected ':'
# | actual at 95: expected ':'
# | actual at 98: expected ':'
# | expect at 82: Repeat specifier before 'LZ' edit descriptor
# | expect at 85: Repeat specifier before 'LZP' edit descriptor
# | expect at 88: Repeat specifier before 'LZS' edit descriptor
# | expect at 92: Repeat specifier before 'LZ' edit descriptor
# | expect at 95: Repeat specifier before 'LZP' edit descriptor
# | expect at 98: Repeat specifier before 'LZS' edit descriptor
# | 
# | FAIL
# `-----------------------------
# error: command failed with exit status: 1

--

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

This test failure is a from an existing issue: #183581

Fixed by the merge of PR #183878.

@laoshd
Copy link
Contributor Author

laoshd commented Mar 6, 2026

🐧 Linux x64 Test Results

  • 4029 tests passed
  • 201 tests skipped
  • 1 test failed

Failed Tests

(click on a test name to see its output)

Flang

Flang.Semantics/io17.f90

Exit Code: 1

Command Output (stdout):
--
# RUN: at line 1
"/usr/bin/python3" /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/test_errors.py /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/io17.f90 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/bin/flang -fc1
# executed command: /usr/bin/python3 /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/test_errors.py /home/gha/actions-runner/_work/llvm-project/llvm-project/flang/test/Semantics/io17.f90 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/bin/flang -fc1
# .---command stdout------------
# | --- 
# | +++ 
# | @@ -1,3 +1,6 @@
# | 
# | actual at 92: expected ':'
# | actual at 95: expected ':'
# | actual at 98: expected ':'
# | expect at 82: Repeat specifier before 'LZ' edit descriptor
# | expect at 85: Repeat specifier before 'LZP' edit descriptor
# | expect at 88: Repeat specifier before 'LZS' edit descriptor
# | expect at 92: Repeat specifier before 'LZ' edit descriptor
# | expect at 95: Repeat specifier before 'LZP' edit descriptor
# | expect at 98: Repeat specifier before 'LZS' edit descriptor
# | 
# | FAIL
# `-----------------------------
# error: command failed with exit status: 1

--

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

This test failure is because issue #183581 (#183581).

Fixed by the merge of PR #183878

@laoshd
Copy link
Contributor Author

laoshd commented Mar 6, 2026

I have updated the title. Please advise if I still missed something. Thanks.

Please read the instructions regarding commit messages in the LLVM Developer Policy. Your title does not adhere to the guidelines. You can also look at other commit messages in the flang subdirectory to get a sense for what this should look like.

Please address the failures in the pre-commit CI below.

The Flang.Semantics test failures were fixed by the merge of PR #183878
The flang-rt test failures were fixed by the last commit, c733fab

// LZP - print leading zero
modes.leadingZero = MutableModes::LeadingZeroMode::Print;
} else {
// LZ - processor-dependent (default behavior)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a distinct enumeration for LZ? It always maps to one of the other two settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LZP will always print the leading zero, LZS will never print, LZ will print or not depending on "processor-defined". This three-way distinction matches the standard's intent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The leading zero mode controls optional leading zero characters in numeric output fields. When the leading zero
mode is PRINT, the processor shall produce a leading zero in any position that normally contains an optional
leading zero. When the leading zero mode is SUPPRESS, the processor shall not produce a leading zero in such
positions. When the leading zero mode is PROCESSOR_DEFINED, the processor has the option of producing
a leading zero or not in such positions, subject to 13.7.2(5)."

template <typename CONTEXT>
static RT_API_ATTRS void HandleControl(
CONTEXT &context, char ch, char next, int n) {
CONTEXT &context, char ch, char next, int n, char next2 = '\0') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there calls to HandleControl() that need this default value for the new parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The default value is unnecessary. Will remove it.

return editingFlags & decimalComma ? char32_t{','} : char32_t{'.'};
}

enum class LeadingZeroMode : std::uint8_t {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could just be a new editing flag, like BN or DP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LZ/LZP/LZS is a three-way choice. It cannot be done by a flag.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LZ means either LZP or LZS, and those two only require one bit to distinguish them.

// point, so the leading zero position is not optional -- skip this.
// Value has no digits before the decimal point: "0.xxxE+yy" vs ".xxxE+yy"
switch (edit.modes.leadingZero) {
case MutableModes::LeadingZeroMode::Print:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't how the edit descriptors' behaviors are defined in 13.8.5. Please conform to the standard when adding new standard features. LZP should behave in the way that you defined LZ, and LZ should behave like LZS or LZP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of the standard is that: LZP means PRINT, alway prints the leading zero; LZS means SUPPRESS, never prints the leading zero; and LZ means PROCESSOR_DEFINED, the processor has option of printing or not (behavior the same as no leading zero edit descriptor is used).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LZP prints a leading zero when there's room for it. LZS doesn't. LZ maps to one or the other of these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I re-implemented it with editingFlags replacing enum, and made it match this request. Would you review it again please.

// Optional leading zero position (F202X leading zero control).
// Value magnitude < 1: "0.xxx" vs ".xxx"
switch (edit.modes.leadingZero) {
case MutableModes::LeadingZeroMode::Print:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. This is not how the modes are defined in the standard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explained above.

@laoshd laoshd requested a review from klausler March 11, 2026 23:55
template <typename CONTEXT>
static RT_API_ATTRS void HandleControl(
CONTEXT &context, char ch, char next, int n) {
CONTEXT &context, char ch, char next, int n, char next2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd that next2 is not the parameter immediately after next.

digitsAfterPoint + trailingZeroes ==
0) {
zeroesBeforePoint = 1; // "." -> "0."
zeroesBeforePoint = 1; // "."--> "0." (bare decimal point)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"." would be a bare decimal point. "0." is not one.

Implementation status:
- `LZ`, `LZS`, `LZP` control edit descriptors, affect only F, E, D, and G
editing of an output statement: Implemented
- `LZ` - Processor-dependent, default (e.g., `0.2`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that this (LZ) is equivalent to LZP.

editing of an output statement: Implemented
- `LZ` - Processor-dependent, default (e.g., `0.2`)
- `LZS` - Suppress leading zeros (e.g., `.2`)
- `LZP` - Print leading zero (e.g. `0.2`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A leading zero is printed only when there is room in the field for it.

- `LZS` - Suppress leading zeros (e.g., `.2`)
- `LZP` - Print leading zero (e.g. `0.2`)
- `AT` edit descriptor: Not yet implemented
- `LEADING_ZERO=specifier` in OPEN statement: Not yet implemented
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This OPEN control specifier should also be part of this change, I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flang:parser flang:semantics flang Flang issues not falling into any other category flang-rt

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants