[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
Fri Jul 31 02:21:49 PDT 2026
https://github.com/ergawy updated https://github.com/llvm/llvm-project/pull/211224
>From cd209ed547cca737b14c7d23fba4c3def8365f42 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/2] [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 +-
flang-rt/unittests/Runtime/Namelist.cpp | 131 +++++++++++++++++++
flang/docs/Extensions.md | 14 ++
4 files changed, 156 insertions(+), 3 deletions(-)
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/unittests/Runtime/Namelist.cpp b/flang-rt/unittests/Runtime/Namelist.cpp
index aaa3c1d354098..5db7c0b477437 100644
--- a/flang-rt/unittests/Runtime/Namelist.cpp
+++ b/flang-rt/unittests/Runtime/Namelist.cpp
@@ -334,6 +334,137 @@ TEST(NamelistTests, RealValueForInt) {
EXPECT_EQ(got, expect);
}
+TEST(NamelistTests, EmptyValueForScalar) {
+ // logical :: l = .true. ; integer :: k = 42
+ // &nml l= k=7/
+ // Exercises Flang's NAMELIST extension that accepts an empty scalar
+ // assignment (F2023 13.11.2 p1 requires one or more values; nvfortran
+ // / gfortran treat the empty form as "keep current value"). Here
+ // `l=` leaves l at .true. and parsing continues with k=7. Using
+ // .true. as the retained value strengthens the test: it distinguishes
+ // "kept" from an accidental default-initialized .false. on error.
+ // 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>{true})};
+ 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= T,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=.true. ;
+ // integer :: arr2(3)=[40,50,60]
+ // &nml arr1=100 200 300 l= arr2=400 500 600/
+ // The empty scalar assignment `l=` sits between two full array
+ // assignments. arr1 must be fully read (three values), then l
+ // retains its .true. value 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})};
+ OwningPtr<Descriptor> lDesc{
+ MakeArray<TypeCategory::Logical, sizeof(std::uint8_t)>(
+ std::vector<int>{}, std::vector<std::uint8_t>{true})};
+ 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= T,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..36977e3f6df00 100644
--- a/flang/docs/Extensions.md
+++ b/flang/docs/Extensions.md
@@ -469,6 +469,20 @@ 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.2 p1 requires
+ one or more values to follow the `=`, but classic nvfortran and
+ gfortran accept the empty form and leave the item's current value
+ unchanged. Flang follows the same convention. For example, given
+ a namelist group `nml` with a `LOGICAL` scalar `l`, an `INTEGER`
+ scalar `i_count`, and a `REAL` scalar `r_value`, the input record
+ ```
+ &nml l= i_count=7 r_value=2.72/
+ ```
+ leaves `l` unchanged and assigns `7` and `2.72` to `i_count` and
+ `r_value` respectively. Without this extension, the runtime would
+ abort with `Bad character 'i' in LOGICAL input field`.
* 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 52bc65b25a2bdaa794429c075e1274de024d8ef9 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/2] [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()) {
More information about the flang-commits
mailing list