[flang-commits] [flang] [llvm] [flang-rt] enable IsNamelistNameOrSlash lookahead for scalar namelist items (PR #211224)

Kareem Ergawy via flang-commits flang-commits at lists.llvm.org
Thu Jul 30 05:57:17 PDT 2026


https://github.com/ergawy updated https://github.com/llvm/llvm-project/pull/211224

>From f86eb5d02df63a7c3593076f5f1bdb3ae1740bfa Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Tue, 21 Jul 2026 07:41:56 -0700
Subject: [PATCH 1/4] [flang-rt][NAMELIST] Accept empty scalar assignments as a
 language extension

Extend Flang's NAMELIST input to accept an assignment to a scalar item
whose value is omitted, e.g. `l=` in

    &nml l= i_count=7 r_value=2.72/

leaving the item's current value unchanged.  This form is non-standard
(F2023 13.11.3.2 requires a value to follow the `=` for a scalar item)
but is accepted by classic nvfortran and gfortran; users porting code
between compilers have come to rely on it.  `flang/docs/Extensions.md`
is updated to list the new NAMELIST extension alongside the existing
`$`/`&` group-start and mid-value `!`-comment extensions.

Implementation
--------------
Every `EditIntegerInput` / `EditRealInput` / `EditLogicalInput` /
`EditCharacterInput` list-directed arm starts with

    if (IsNamelistNameOrSlash(io)) return false;   // no value

which peeks ahead (via `SavedPosition`, no stream consumption) for a
`<name>=` / `<name>%` / `<name>(` shape or one of the terminators
`/` `&` `$`, letting the reader bail cleanly for empty values and
short-array ends.  The helper's first line is

    if (!listInput || !listInput->namelistGroup()) return false;

`InputNamelist` however called `ResetForNextNamelistItem` with
`useDescriptor->rank() > 0 ? &group : nullptr`, so `namelistGroup_`
stayed null for scalars.  The peek was silently disabled and the
value reader consumed the next name-value pair's name as a bare token,
producing a "Bad character" runtime abort.

Pass `&group` unconditionally to `ResetForNextNamelistItem`.  Today
`IsNamelistNameOrSlash` uses `namelistGroup_` only as a boolean gate
(never as a lookup table), so widening it is a no-op for arrays and
enables the same empty-value / next-name detection for scalars.
`NamelistTests.NanInputAmbiguity` (which motivated the original
pointer form) still passes; three new tests cover the empty-scalar
case, the empty-array case, and an empty scalar surrounded by arrays.
---
 flang-rt/include/flang-rt/runtime/namelist.h  |   2 +-
 flang-rt/lib/runtime/namelist.cpp             |  12 +-
 .../Driver/namelist-empty-scalar-logical.f90  |  25 ++++
 flang-rt/unittests/Runtime/Namelist.cpp       | 125 ++++++++++++++++++
 flang/docs/Extensions.md                      |   6 +
 5 files changed, 167 insertions(+), 3 deletions(-)
 create mode 100644 flang-rt/test/Driver/namelist-empty-scalar-logical.f90

diff --git a/flang-rt/include/flang-rt/runtime/namelist.h b/flang-rt/include/flang-rt/runtime/namelist.h
index 17d7bf310cc96..3feb8440f077b 100644
--- a/flang-rt/include/flang-rt/runtime/namelist.h
+++ b/flang-rt/include/flang-rt/runtime/namelist.h
@@ -18,10 +18,10 @@
 
 namespace Fortran::runtime {
 class Descriptor;
-class IoStatementState;
 } // namespace Fortran::runtime
 
 namespace Fortran::runtime::io {
+class IoStatementState;
 
 // A NAMELIST group is a named ordered collection of distinct variable names.
 // It is packaged by lowering into an instance of this class.
diff --git a/flang-rt/lib/runtime/namelist.cpp b/flang-rt/lib/runtime/namelist.cpp
index c1745595b88f4..d644a9fb1f049 100644
--- a/flang-rt/lib/runtime/namelist.cpp
+++ b/flang-rt/lib/runtime/namelist.cpp
@@ -612,8 +612,16 @@ bool IODEF(InputNamelist)(Cookie cookie, const NamelistGroup &group) {
         return false;
       }
     } else {
-      listInput->ResetForNextNamelistItem(
-          useDescriptor->rank() > 0 ? &group : nullptr);
+      // Pass &group unconditionally (not just for arrays) so the
+      // IsNamelistNameOrSlash look-ahead in Edit{Integer,Real,Logical,
+      // Character}Input fires for scalar items too.  Each of those
+      // per-type value readers starts its list-directed arm with
+      //
+      //     if (IsNamelistNameOrSlash(io)) return false;   // no value
+      //
+      // With &group set, that empty-value probe works for scalars as well as
+      // sequences.
+      listInput->ResetForNextNamelistItem(&group);
       if (!descr::DescriptorIO<Direction::Input>(io, *useDescriptor) &&
           handler.InError()) {
         return false;
diff --git a/flang-rt/test/Driver/namelist-empty-scalar-logical.f90 b/flang-rt/test/Driver/namelist-empty-scalar-logical.f90
new file mode 100644
index 0000000000000..411e16b9f867f
--- /dev/null
+++ b/flang-rt/test/Driver/namelist-empty-scalar-logical.f90
@@ -0,0 +1,25 @@
+! UNSUPPORTED: offload-cuda
+
+! Regression test for a runtime bug in InputNamelist where an empty
+! assignment to an item aborted with
+!   fatal Fortran runtime error: Bad character 'i' in LOGICAL input field
+! The empty value should leave the item at its current value and parsing
+! should continue with the next assignment.
+
+! RUN: %flang %isysroot -L"%libdir" %s -o %t
+! RUN: env LD_LIBRARY_PATH="$LD_LIBRARY_PATH:%libdir" %t | FileCheck %s
+
+! CHECK: l_flag=F
+! CHECK-NEXT: i_count=7
+program p
+  implicit none
+  logical :: l_flag = .false.
+  integer :: i_count = 42
+  namelist /test_nml/ l_flag, i_count
+  character(len=64) :: buf = "&test_nml l_flag= i_count=7 /"
+
+  read(buf, nml=test_nml)
+
+  print '(a,l1)', 'l_flag=', l_flag
+  print '(a,i0)', 'i_count=', i_count
+end program
diff --git a/flang-rt/unittests/Runtime/Namelist.cpp b/flang-rt/unittests/Runtime/Namelist.cpp
index aaa3c1d354098..12f53e9e29db7 100644
--- a/flang-rt/unittests/Runtime/Namelist.cpp
+++ b/flang-rt/unittests/Runtime/Namelist.cpp
@@ -334,6 +334,131 @@ TEST(NamelistTests, RealValueForInt) {
   EXPECT_EQ(got, expect);
 }
 
+TEST(NamelistTests, EmptyValueForScalar) {
+  // logical :: l = .false. ; integer :: k = 42
+  //   &nml l= k=7/
+  // The empty assignment `l=` should leave l at its .false. default and
+  // parsing should continue with k=7.  Regression test for a bug where
+  // scalar namelist items disabled the IsNamelistNameOrSlash guard in
+  // Edit*Input, so l's parse consumed the `k` on the next token and
+  // signalled "Bad character 'k' in LOGICAL input field".
+  OwningPtr<Descriptor> lDesc{
+      MakeArray<TypeCategory::Logical, sizeof(std::uint8_t)>(
+          std::vector<int>{}, std::vector<std::uint8_t>{false})};
+  OwningPtr<Descriptor> kDesc{
+      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
+          std::vector<int>{}, std::vector<int>{42})};
+  const NamelistGroup::Item items[]{{"l", *lDesc}, {"k", *kDesc}};
+  const NamelistGroup group{"nml", 2, items};
+  static char t1[]{"&nml l= k=7/"};
+  StaticDescriptor<1, true> statDesc;
+  Descriptor &internalDesc{statDesc.descriptor()};
+  internalDesc.Establish(TypeCode{CFI_type_char},
+      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
+  auto inCookie{IONAME(BeginInternalArrayListInput)(
+      internalDesc, nullptr, 0, __FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
+  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
+      << "namelist empty scalar assignment";
+  char out[24];
+  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
+      out, 0, nullptr, CFI_attribute_pointer);
+  auto outCookie{IONAME(BeginInternalArrayListOutput)(
+      internalDesc, nullptr, 0, __FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
+  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
+  std::string got{out, sizeof out};
+  static const std::string expect{" &NML L= F,K= 7/        "};
+  EXPECT_EQ(got, expect);
+}
+
+TEST(NamelistTests, EmptyValueForArray) {
+  // integer :: k=1 ; integer :: arr(3)=[10,20,30] ; integer :: m=2
+  //   &nml k=100 arr= m=200/
+  // The empty assignment `arr=` should leave the array at its default and
+  // parsing should continue with m=200.  This case worked before the
+  // `rank() > 0 ? &group : nullptr` filter was widened, but is guarded
+  // here to make sure widening the pointer for scalars didn't regress the
+  // short-array end-of-values detection.
+  OwningPtr<Descriptor> kDesc{
+      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
+          std::vector<int>{}, std::vector<int>{1})};
+  OwningPtr<Descriptor> arrDesc{
+      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
+          std::vector<int>{3}, std::vector<int>{10, 20, 30})};
+  OwningPtr<Descriptor> mDesc{
+      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
+          std::vector<int>{}, std::vector<int>{2})};
+  const NamelistGroup::Item items[]{
+      {"k", *kDesc}, {"arr", *arrDesc}, {"m", *mDesc}};
+  const NamelistGroup group{"nml", 3, items};
+  static char t1[]{"&nml k=100 arr= m=200/"};
+  StaticDescriptor<1, true> statDesc;
+  Descriptor &internalDesc{statDesc.descriptor()};
+  internalDesc.Establish(TypeCode{CFI_type_char},
+      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
+  auto inCookie{IONAME(BeginInternalArrayListInput)(
+      internalDesc, nullptr, 0, __FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
+  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
+      << "namelist empty array assignment";
+  char out[48];
+  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
+      out, 0, nullptr, CFI_attribute_pointer);
+  auto outCookie{IONAME(BeginInternalArrayListOutput)(
+      internalDesc, nullptr, 0, __FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
+  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
+  std::string got{out, sizeof out};
+  static const std::string expect{
+      " &NML K= 100,ARR= 10 20 30,M= 200/              "};
+  EXPECT_EQ(got, expect);
+}
+
+TEST(NamelistTests, EmptyScalarBetweenArrays) {
+  // integer :: arr1(3)=[10,20,30] ; logical :: l=.false. ;
+  // integer :: arr2(3)=[40,50,60]
+  //   &nml arr1=100 200 300 l= arr2=400 500 600/
+  // The empty assignment `l=` sits between two full array assignments.
+  // arr1 must be fully read (three values), then l retains its default
+  // (empty scalar), then arr2 must be fully read.  Exercises the
+  // interaction between the widened scalar guard and the array
+  // short-value / end-of-values detection.
+  OwningPtr<Descriptor> arr1Desc{
+      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
+          std::vector<int>{3}, std::vector<int>{10, 20, 30})};
+  OwningPtr<Descriptor> lDesc{
+      MakeArray<TypeCategory::Logical, sizeof(std::uint8_t)>(
+          std::vector<int>{}, std::vector<std::uint8_t>{false})};
+  OwningPtr<Descriptor> arr2Desc{
+      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
+          std::vector<int>{3}, std::vector<int>{40, 50, 60})};
+  const NamelistGroup::Item items[]{
+      {"arr1", *arr1Desc}, {"l", *lDesc}, {"arr2", *arr2Desc}};
+  const NamelistGroup group{"nml", 3, items};
+  static char t1[]{"&nml arr1=100 200 300 l= arr2=400 500 600/"};
+  StaticDescriptor<1, true> statDesc;
+  Descriptor &internalDesc{statDesc.descriptor()};
+  internalDesc.Establish(TypeCode{CFI_type_char},
+      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
+  auto inCookie{IONAME(BeginInternalArrayListInput)(
+      internalDesc, nullptr, 0, __FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
+  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
+      << "namelist empty scalar between arrays";
+  char out[64];
+  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
+      out, 0, nullptr, CFI_attribute_pointer);
+  auto outCookie{IONAME(BeginInternalArrayListOutput)(
+      internalDesc, nullptr, 0, __FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
+  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
+  std::string got{out, sizeof out};
+  static const std::string expect{
+      " &NML ARR1= 100 200 300,L= F,ARR2= 400 500 600/                 "};
+  EXPECT_EQ(got, expect);
+}
+
 TEST(NamelistTests, NanInputAmbiguity) {
   OwningPtr<Descriptor> xDesc{// real :: x(5) = 0.
       MakeArray<TypeCategory::Real, static_cast<int>(sizeof(float))>(
diff --git a/flang/docs/Extensions.md b/flang/docs/Extensions.md
index 054e38c623bca..1075ebf748ab3 100644
--- a/flang/docs/Extensions.md
+++ b/flang/docs/Extensions.md
@@ -469,6 +469,12 @@ print *, is_contiguous(a(::2))                   ! prints T in Flang
 * A `NAMELIST` input group may omit its trailing `/` character if
   it is followed by another `NAMELIST` input group.
 * A `NAMELIST` input group may begin with either `&` or `$`.
+* In `NAMELIST` input, an assignment to a scalar item may omit its
+  value (e.g. `l=`, immediately followed by the next name-value pair,
+  the group terminator, or end-of-record).  F2023 13.11.3.2 requires a
+  value to follow the `=` for a scalar item, but classic nvfortran and
+  gfortran accept the empty form and leave the item's current value
+  unchanged.  Flang follows the same convention.
 * In `NAMELIST` input, a `!` character is accepted as terminating the
   current value and introducing a comment even when it is not preceded
   by a value separator.  For example, `name=0.01!comment` is accepted

>From 330a9c3c0ee9edcc101f5dc9265dfac9e40e4012 Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Thu, 30 Jul 2026 04:00:10 -0700
Subject: [PATCH 2/4] [flang-rt] Reword ResetForNextNamelistItem comment as an
 extension

---
 flang-rt/lib/runtime/namelist.cpp | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/flang-rt/lib/runtime/namelist.cpp b/flang-rt/lib/runtime/namelist.cpp
index d644a9fb1f049..231b1705a4a2f 100644
--- a/flang-rt/lib/runtime/namelist.cpp
+++ b/flang-rt/lib/runtime/namelist.cpp
@@ -619,8 +619,11 @@ bool IODEF(InputNamelist)(Cookie cookie, const NamelistGroup &group) {
       //
       //     if (IsNamelistNameOrSlash(io)) return false;   // no value
       //
-      // With &group set, that empty-value probe works for scalars as well as
-      // sequences.
+      // With &group set, the empty-value probe works for scalars as
+      // well as sequences.  This implements Flang's NAMELIST extension
+      // that accepts an empty scalar assignment (e.g. `l=` immediately
+      // followed by the next name-value pair or the group terminator)
+      // as "keep current value" — see flang/docs/Extensions.md.
       listInput->ResetForNextNamelistItem(&group);
       if (!descr::DescriptorIO<Direction::Input>(io, *useDescriptor) &&
           handler.InError()) {

>From af7fef717c5fe7931238058ca88dea62fa728342 Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Thu, 30 Jul 2026 04:00:11 -0700
Subject: [PATCH 3/4] [flang-rt] Reword NamelistTests comments as an extension

---
 flang-rt/unittests/Runtime/Namelist.cpp | 34 ++++++++++++++-----------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/flang-rt/unittests/Runtime/Namelist.cpp b/flang-rt/unittests/Runtime/Namelist.cpp
index 12f53e9e29db7..0dfba81f8532e 100644
--- a/flang-rt/unittests/Runtime/Namelist.cpp
+++ b/flang-rt/unittests/Runtime/Namelist.cpp
@@ -337,11 +337,14 @@ TEST(NamelistTests, RealValueForInt) {
 TEST(NamelistTests, EmptyValueForScalar) {
   // logical :: l = .false. ; integer :: k = 42
   //   &nml l= k=7/
-  // The empty assignment `l=` should leave l at its .false. default and
-  // parsing should continue with k=7.  Regression test for a bug where
-  // scalar namelist items disabled the IsNamelistNameOrSlash guard in
-  // Edit*Input, so l's parse consumed the `k` on the next token and
-  // signalled "Bad character 'k' in LOGICAL input field".
+  // Exercises Flang's NAMELIST extension that accepts an empty scalar
+  // assignment (F2023 13.11.3.2 requires a value; nvfortran / gfortran
+  // treat the empty form as "keep current value").  Here `l=` leaves l
+  // at .false. and parsing continues with k=7.  Before the extension
+  // was implemented, scalar namelist items disabled the
+  // IsNamelistNameOrSlash guard in Edit*Input, so l's parse would
+  // consume the `k` token and signal "Bad character 'k' in LOGICAL
+  // input field".
   OwningPtr<Descriptor> lDesc{
       MakeArray<TypeCategory::Logical, sizeof(std::uint8_t)>(
           std::vector<int>{}, std::vector<std::uint8_t>{false})};
@@ -375,11 +378,11 @@ TEST(NamelistTests, EmptyValueForScalar) {
 TEST(NamelistTests, EmptyValueForArray) {
   // integer :: k=1 ; integer :: arr(3)=[10,20,30] ; integer :: m=2
   //   &nml k=100 arr= m=200/
-  // The empty assignment `arr=` should leave the array at its default and
-  // parsing should continue with m=200.  This case worked before the
-  // `rank() > 0 ? &group : nullptr` filter was widened, but is guarded
-  // here to make sure widening the pointer for scalars didn't regress the
-  // short-array end-of-values detection.
+  // Array items already accepted the empty form as an end-of-values
+  // marker before the scalar extension landed; this test guards that
+  // path so widening the IsNamelistNameOrSlash pointer for scalars
+  // preserves the existing array short-value / end-of-values
+  // detection.
   OwningPtr<Descriptor> kDesc{
       MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
           std::vector<int>{}, std::vector<int>{1})};
@@ -419,11 +422,12 @@ TEST(NamelistTests, EmptyScalarBetweenArrays) {
   // integer :: arr1(3)=[10,20,30] ; logical :: l=.false. ;
   // integer :: arr2(3)=[40,50,60]
   //   &nml arr1=100 200 300 l= arr2=400 500 600/
-  // The empty assignment `l=` sits between two full array assignments.
-  // arr1 must be fully read (three values), then l retains its default
-  // (empty scalar), then arr2 must be fully read.  Exercises the
-  // interaction between the widened scalar guard and the array
-  // short-value / end-of-values detection.
+  // The empty scalar assignment `l=` sits between two full array
+  // assignments.  arr1 must be fully read (three values), then l
+  // retains its default under the empty-scalar extension, then arr2
+  // must be fully read.  Exercises the interaction between the scalar
+  // extension and the pre-existing array short-value / end-of-values
+  // detection.
   OwningPtr<Descriptor> arr1Desc{
       MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
           std::vector<int>{3}, std::vector<int>{10, 20, 30})};

>From 89237db30bd5ec94a78821c8ee735df9f55ab38f Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Thu, 30 Jul 2026 04:00:11 -0700
Subject: [PATCH 4/4] [flang-rt] Reword namelist-empty-scalar-logical driver
 test header as an extension

---
 .../Driver/namelist-empty-scalar-logical.f90  | 24 +++++++++++++------
 1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/flang-rt/test/Driver/namelist-empty-scalar-logical.f90 b/flang-rt/test/Driver/namelist-empty-scalar-logical.f90
index 411e16b9f867f..6b165ed675de5 100644
--- a/flang-rt/test/Driver/namelist-empty-scalar-logical.f90
+++ b/flang-rt/test/Driver/namelist-empty-scalar-logical.f90
@@ -1,25 +1,35 @@
 ! UNSUPPORTED: offload-cuda
 
-! Regression test for a runtime bug in InputNamelist where an empty
-! assignment to an item aborted with
-!   fatal Fortran runtime error: Bad character 'i' in LOGICAL input field
-! The empty value should leave the item at its current value and parsing
-! should continue with the next assignment.
+! Reads two NAMELIST records whose scalar LOGICAL item has an empty
+! assignment — first `l_flag=` with no spaces around the `=`, then
+! `l_flag = ` with surrounding whitespace — and checks that in both
+! cases l_flag keeps its .false. default while i_count picks up 7
+! from the following assignment.  Covers the Flang NAMELIST
+! empty-scalar extension listed in flang/docs/Extensions.md.
 
 ! RUN: %flang %isysroot -L"%libdir" %s -o %t
 ! RUN: env LD_LIBRARY_PATH="$LD_LIBRARY_PATH:%libdir" %t | FileCheck %s
 
 ! CHECK: l_flag=F
 ! CHECK-NEXT: i_count=7
+! CHECK-NEXT: l_flag=F
+! CHECK-NEXT: i_count=7
 program p
   implicit none
   logical :: l_flag = .false.
   integer :: i_count = 42
   namelist /test_nml/ l_flag, i_count
-  character(len=64) :: buf = "&test_nml l_flag= i_count=7 /"
+  character(len=64) :: buf_tight  = "&test_nml l_flag= i_count=7 /"
+  character(len=64) :: buf_spaced = "&test_nml l_flag = i_count = 7 /"
 
-  read(buf, nml=test_nml)
+  read(buf_tight, nml=test_nml)
+  print '(a,l1)', 'l_flag=', l_flag
+  print '(a,i0)', 'i_count=', i_count
 
+  ! Reset the defaults and re-read from the whitespace-decorated form.
+  l_flag = .false.
+  i_count = 42
+  read(buf_spaced, nml=test_nml)
   print '(a,l1)', 'l_flag=', l_flag
   print '(a,i0)', 'i_count=', i_count
 end program



More information about the flang-commits mailing list