[flang-commits] [flang] [flang] Suggest -flogical-abbreviations when .T./.F. fails to parse (PR #205232)

Eugene Epshteyn via flang-commits flang-commits at lists.llvm.org
Fri Jul 3 14:57:22 PDT 2026


https://github.com/eugeneepshteyn updated https://github.com/llvm/llvm-project/pull/205232

>From 3599424eae51714b17289f05d324906d79503983 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Mon, 22 Jun 2026 21:18:22 -0400
Subject: [PATCH 1/3] [flang] Suggest -flogical-abbreviations when .T./.F.
 fails to parse

The logical abbreviations .T. and .F. (for .TRUE. and .FALSE.) are a
nonstandard extension that flang accepts only under
-flogical-abbreviations, which is disabled by default because these
spellings -- and the operator forms .N./.A./.O./.X. -- are also valid
defined-operator names.

Of these, only the literals .T. and .F. cause a parse failure, since
the operator forms parse as defined operators and instead fail later
in semantics ("No operator .A. defined").

Consider the following code: `logical :: flag = .F.`

When -flogical-abbreviations is off, such code may cause a confusing
cascade of errors that dosn't give the user a clue about the option
that would accept it. The new implementation keeps the existing
diagnostics and additionally emits a note pointing at the abbreviation:

  This nonstandard logical abbreviation requires the
  '-flogical-abbreviations' option

Assisted-by: AI
---
 flang/include/flang/Parser/message.h          |  9 ++++
 flang/include/flang/Parser/parsing.h          |  7 +++
 flang/include/flang/Parser/user-state.h       | 19 ++++++++
 flang/lib/Parser/basic-parsers.h              | 22 +++++++++
 flang/lib/Parser/parsing.cpp                  | 46 ++++++++++++++++++-
 .../logical-abbreviation-defined-operator.f90 | 23 ++++++++++
 .../logical-abbreviation-no-suggestion.f90    | 25 ++++++++++
 .../logical-abbreviation-suggestion-true.f90  | 13 ++++++
 .../Parser/logical-abbreviation-suggestion.f  | 13 ++++++
 .../logical-abbreviation-suggestion.f90       | 13 ++++++
 10 files changed, 189 insertions(+), 1 deletion(-)
 create mode 100644 flang/test/Parser/logical-abbreviation-defined-operator.f90
 create mode 100644 flang/test/Parser/logical-abbreviation-no-suggestion.f90
 create mode 100644 flang/test/Parser/logical-abbreviation-suggestion-true.f90
 create mode 100644 flang/test/Parser/logical-abbreviation-suggestion.f
 create mode 100644 flang/test/Parser/logical-abbreviation-suggestion.f90

diff --git a/flang/include/flang/Parser/message.h b/flang/include/flang/Parser/message.h
index c70c335133ba8..4c87f58e163a5 100644
--- a/flang/include/flang/Parser/message.h
+++ b/flang/include/flang/Parser/message.h
@@ -292,6 +292,15 @@ class Message : public common::ReferenceCounted<Message> {
     return Attach(new Message{std::forward<A>(args)...}); // reference-counted
   }
 
+  // During parsing (before ResolveProvenances) a message locates itself with a
+  // CharBlock into the cooked source; returns it when present.
+  std::optional<CharBlock> GetCookedSourceLocation() const {
+    if (const CharBlock *cb{std::get_if<CharBlock>(&location_)}) {
+      return *cb;
+    }
+    return std::nullopt;
+  }
+
   bool SortBefore(const Message &that) const;
   bool IsFatal() const;
   Severity severity() const;
diff --git a/flang/include/flang/Parser/parsing.h b/flang/include/flang/Parser/parsing.h
index 3365628dc4e0c..c48531c93f9d1 100644
--- a/flang/include/flang/Parser/parsing.h
+++ b/flang/include/flang/Parser/parsing.h
@@ -21,6 +21,8 @@
 
 namespace Fortran::parser {
 
+class UserState;
+
 class Parsing {
 public:
   explicit Parsing(AllCookedSources &);
@@ -55,6 +57,11 @@ class Parsing {
   }
 
 private:
+  // Adds a note suggesting -flogical-abbreviations when the parse failed on a
+  // source line bearing a logical abbreviation (.T./.F./.N./.A./.O.) that the
+  // disabled LogicalAbbreviations feature would otherwise accept.
+  void SuggestLogicalAbbreviations(const UserState &, Messages &);
+
   Options options_;
   AllCookedSources &allCooked_;
   CookedSource *currentCooked_{nullptr};
diff --git a/flang/include/flang/Parser/user-state.h b/flang/include/flang/Parser/user-state.h
index 129f9fb8fee05..1642655884283 100644
--- a/flang/include/flang/Parser/user-state.h
+++ b/flang/include/flang/Parser/user-state.h
@@ -89,6 +89,24 @@ class UserState {
     return oldStructureComponents_.find(name) != oldStructureComponents_.end();
   }
 
+  // When the LogicalAbbreviations feature is disabled, the parser records the
+  // source location of any logical abbreviation (.T./.F./.N./.A./.O.) it
+  // encounters, so that a parse failure on that same source line can suggest
+  // enabling the -flogical-abbreviations option.  Matching by source line
+  // (rather than by exact position) is deliberate: a misparse triggered by an
+  // abbreviation typically reports its failure a few columns away from the
+  // abbreviation itself (e.g. 'OnGPU = .F.' is first misread as a statement
+  // function, so the error points at the '='), while requiring the same line
+  // still prevents the hint from firing on an unrelated failure elsewhere in a
+  // file that merely uses such a spelling as a defined operator.
+  void NoteDisabledLogicalAbbreviation(CharBlock at) {
+    disabledLogicalAbbreviations_.insert(at);
+  }
+  // Recorded in increasing source order (std::set ordered by CharBlock).
+  const std::set<CharBlock> &disabledLogicalAbbreviations() const {
+    return disabledLogicalAbbreviations_;
+  }
+
 private:
   const AllCookedSources &allCooked_;
 
@@ -101,6 +119,7 @@ class UserState {
   int nonlabelDoConstructNestingDepth_{0};
 
   std::set<CharBlock> oldStructureComponents_;
+  std::set<CharBlock> disabledLogicalAbbreviations_;
 
   common::LanguageFeatureControl features_;
 };
diff --git a/flang/lib/Parser/basic-parsers.h b/flang/lib/Parser/basic-parsers.h
index eeb59a830fc0c..c7a6873da4ed5 100644
--- a/flang/lib/Parser/basic-parsers.h
+++ b/flang/lib/Parser/basic-parsers.h
@@ -848,6 +848,28 @@ template <LanguageFeature LF, typename PA> class NonstandardParser {
   std::optional<resultType> Parse(ParseState &state) const {
     if (UserState * ustate{state.userState()}) {
       if (!ustate->features().IsEnabled(LF)) {
+        if constexpr (LF == LanguageFeature::LogicalAbbreviations) {
+          // The feature is disabled, but if the source actually spells a
+          // logical abbreviation here, remember its location so that a later
+          // parse failure at this spot can suggest -flogical-abbreviations.
+          // Every such abbreviation begins with '.', so only attempt the
+          // speculative parse (which copies the parse state) when the next
+          // non-blank character could start one; this keeps the common case --
+          // a primary or operator that is not an abbreviation -- cheap.
+          const char *at{state.GetLocation()};
+          const char *p{at};
+          const char *limit{at + state.BytesRemaining()};
+          while (p < limit && *p == ' ') {
+            ++p;
+          }
+          if (p < limit && *p == '.') {
+            ParseState fork{state};
+            if (parser_.Parse(fork)) {
+              ustate->NoteDisabledLogicalAbbreviation(
+                  CharBlock{at, std::max(fork.GetLocation(), at + 1)});
+            }
+          }
+        }
         return std::nullopt;
       }
     }
diff --git a/flang/lib/Parser/parsing.cpp b/flang/lib/Parser/parsing.cpp
index 667d8d9297ecb..3e879c35e2f4b 100644
--- a/flang/lib/Parser/parsing.cpp
+++ b/flang/lib/Parser/parsing.cpp
@@ -13,7 +13,10 @@
 #include "flang/Parser/preprocessor.h"
 #include "flang/Parser/provenance.h"
 #include "flang/Parser/source.h"
+#include "flang/Parser/user-state.h"
 #include "llvm/Support/raw_ostream.h"
+#include <set>
+#include <utility>
 
 namespace Fortran::parser {
 
@@ -292,8 +295,49 @@ void Parsing::Parse(llvm::raw_ostream &out) {
   CHECK(
       !parseState.anyErrorRecovery() || parseState.messages().AnyFatalError());
   consumedWholeFile_ = parseState.IsAtEnd();
-  messages_.Annex(std::move(parseState.messages()));
   finalRestingPlace_ = parseState.GetLocation();
+  SuggestLogicalAbbreviations(userState, parseState.messages());
+  messages_.Annex(std::move(parseState.messages()));
+}
+
+// When the LogicalAbbreviations feature is disabled, the parser records the
+// location of every logical abbreviation (.T./.F./.N./.A./.O.) it sees.  If the
+// parse then failed on a source line where such a spelling appears, suggest the
+// -flogical-abbreviations option.  Matching the suggestion to a failing source
+// line (rather than firing on any recorded spelling) keeps it from appearing
+// when an unrelated failure occurs in a file that legitimately uses such a
+// spelling as a defined operator on a different line.  Only one suggestion is
+// emitted, anchored at the first such abbreviation in source order.
+void Parsing::SuggestLogicalAbbreviations(
+    const UserState &userState, Messages &messages) {
+  const std::set<CharBlock> &abbreviations{
+      userState.disabledLogicalAbbreviations()};
+  if (abbreviations.empty()) {
+    return;
+  }
+  std::set<std::pair<const SourceFile *, int>> errorLines;
+  for (const Message &message : messages.messages()) {
+    if (!message.IsFatal()) {
+      continue;
+    }
+    if (std::optional<CharBlock> errorLoc{message.GetCookedSourceLocation()}) {
+      if (auto pos{allCooked_.GetSourcePositionRange(*errorLoc)}) {
+        errorLines.emplace(&*pos->first.sourceFile, pos->first.line);
+      }
+    }
+  }
+  if (errorLines.empty()) {
+    return;
+  }
+  for (const CharBlock &abbreviation : abbreviations) {
+    if (auto pos{allCooked_.GetSourcePositionRange(abbreviation)}) {
+      if (errorLines.count({&*pos->first.sourceFile, pos->first.line}) > 0) {
+        messages.Say(abbreviation,
+            "This nonstandard logical abbreviation requires the '-flogical-abbreviations' option"_en_US);
+        return;
+      }
+    }
+  }
 }
 
 void Parsing::ClearLog() { log_.clear(); }
diff --git a/flang/test/Parser/logical-abbreviation-defined-operator.f90 b/flang/test/Parser/logical-abbreviation-defined-operator.f90
new file mode 100644
index 0000000000000..ba3a59100910b
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-defined-operator.f90
@@ -0,0 +1,23 @@
+! A program that legitimately uses .f. as a defined unary operator parses and
+! compiles cleanly with the default (LogicalAbbreviations disabled).  The
+! abbreviation spelling is recorded by the parser, but because the parse
+! succeeds no -flogical-abbreviations suggestion is emitted.
+
+! RUN: %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s --allow-empty --implicit-check-not='-flogical-abbreviations'
+
+module m
+  interface operator(.f.)
+    module procedure neg
+  end interface
+contains
+  pure integer function neg(a)
+    integer, intent(in) :: a
+    neg = -a
+  end function
+end module
+program p
+  use m
+  integer :: r
+  r = .f. 4
+  print *, r
+end program
diff --git a/flang/test/Parser/logical-abbreviation-no-suggestion.f90 b/flang/test/Parser/logical-abbreviation-no-suggestion.f90
new file mode 100644
index 0000000000000..dec7e78ed9aea
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-no-suggestion.f90
@@ -0,0 +1,25 @@
+! Mitigation test: .f. is used here as a legitimate defined unary operator on
+! one line, while an unrelated parse error occurs on a different line.  Because
+! the failure is not on the line bearing the abbreviation, the
+! -flogical-abbreviations suggestion must NOT be emitted.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s --implicit-check-not='-flogical-abbreviations'
+
+module m
+  interface operator(.f.)
+    module procedure neg
+  end interface
+contains
+  pure integer function neg(a)
+    integer, intent(in) :: a
+    neg = -a
+  end function
+end module
+program p
+  use m
+  integer :: r
+  r = .f. 4
+  this is not valid
+end program
+
+! CHECK: error:
diff --git a/flang/test/Parser/logical-abbreviation-suggestion-true.f90 b/flang/test/Parser/logical-abbreviation-suggestion-true.f90
new file mode 100644
index 0000000000000..59b2d2de1471d
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-suggestion-true.f90
@@ -0,0 +1,13 @@
+! The .T. spelling of .TRUE. is handled like .F.: the usual parse error plus a
+! suggestion to enable -flogical-abbreviations.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s
+! RUN: %flang_fc1 -fsyntax-only -flogical-abbreviations %s
+
+program p
+  logical :: x
+  x = .T.
+end program
+
+! CHECK: error: expected '('
+! CHECK: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option
diff --git a/flang/test/Parser/logical-abbreviation-suggestion.f b/flang/test/Parser/logical-abbreviation-suggestion.f
new file mode 100644
index 0000000000000..ef08d744109c5
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-suggestion.f
@@ -0,0 +1,13 @@
+! Fixed-form counterpart: the .F. abbreviation in an assignment is first
+! misparsed as a statement function (so the error points at the '='), but the
+! suggestion is still anchored at the abbreviation and offered to the user.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s
+! RUN: %flang_fc1 -fsyntax-only -flogical-abbreviations %s
+
+      logical flag
+      flag = .F.
+      end
+
+! CHECK: error: expected '('
+! CHECK: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option
diff --git a/flang/test/Parser/logical-abbreviation-suggestion.f90 b/flang/test/Parser/logical-abbreviation-suggestion.f90
new file mode 100644
index 0000000000000..ad59243fc5237
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-suggestion.f90
@@ -0,0 +1,13 @@
+! Verify that a parse failure caused by the disabled .F. logical abbreviation
+! still reports the usual errors and additionally suggests the
+! -flogical-abbreviations option, and that enabling the option compiles cleanly.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s
+! RUN: %flang_fc1 -fsyntax-only -flogical-abbreviations %s
+
+logical :: x
+x = .F.
+end
+
+! CHECK: error: expected '('
+! CHECK: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option

>From 3cd56fe0b4e86e75b27eab316c064635087aa52a Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Fri, 3 Jul 2026 12:32:27 -0400
Subject: [PATCH 2/3] Better handling of multiple occurrenses of options
 needing logical abbreviations

---
 flang/include/flang/Parser/user-state.h         |  8 +-------
 flang/lib/Parser/basic-parsers.h                |  3 +--
 flang/lib/Parser/parsing.cpp                    | 17 +++++++++--------
 ...logical-abbreviation-suggestion-multiple.f90 | 15 +++++++++++++++
 4 files changed, 26 insertions(+), 17 deletions(-)
 create mode 100644 flang/test/Parser/logical-abbreviation-suggestion-multiple.f90

diff --git a/flang/include/flang/Parser/user-state.h b/flang/include/flang/Parser/user-state.h
index 1642655884283..056e1fccd8059 100644
--- a/flang/include/flang/Parser/user-state.h
+++ b/flang/include/flang/Parser/user-state.h
@@ -92,13 +92,7 @@ class UserState {
   // When the LogicalAbbreviations feature is disabled, the parser records the
   // source location of any logical abbreviation (.T./.F./.N./.A./.O.) it
   // encounters, so that a parse failure on that same source line can suggest
-  // enabling the -flogical-abbreviations option.  Matching by source line
-  // (rather than by exact position) is deliberate: a misparse triggered by an
-  // abbreviation typically reports its failure a few columns away from the
-  // abbreviation itself (e.g. 'OnGPU = .F.' is first misread as a statement
-  // function, so the error points at the '='), while requiring the same line
-  // still prevents the hint from firing on an unrelated failure elsewhere in a
-  // file that merely uses such a spelling as a defined operator.
+  // enabling the -flogical-abbreviations option.
   void NoteDisabledLogicalAbbreviation(CharBlock at) {
     disabledLogicalAbbreviations_.insert(at);
   }
diff --git a/flang/lib/Parser/basic-parsers.h b/flang/lib/Parser/basic-parsers.h
index c7a6873da4ed5..800fb74b8a023 100644
--- a/flang/lib/Parser/basic-parsers.h
+++ b/flang/lib/Parser/basic-parsers.h
@@ -854,8 +854,7 @@ template <LanguageFeature LF, typename PA> class NonstandardParser {
           // parse failure at this spot can suggest -flogical-abbreviations.
           // Every such abbreviation begins with '.', so only attempt the
           // speculative parse (which copies the parse state) when the next
-          // non-blank character could start one; this keeps the common case --
-          // a primary or operator that is not an abbreviation -- cheap.
+          // non-blank character could start one.
           const char *at{state.GetLocation()};
           const char *p{at};
           const char *limit{at + state.BytesRemaining()};
diff --git a/flang/lib/Parser/parsing.cpp b/flang/lib/Parser/parsing.cpp
index 3e879c35e2f4b..12ca0009e4700 100644
--- a/flang/lib/Parser/parsing.cpp
+++ b/flang/lib/Parser/parsing.cpp
@@ -301,13 +301,12 @@ void Parsing::Parse(llvm::raw_ostream &out) {
 }
 
 // When the LogicalAbbreviations feature is disabled, the parser records the
-// location of every logical abbreviation (.T./.F./.N./.A./.O.) it sees.  If the
-// parse then failed on a source line where such a spelling appears, suggest the
-// -flogical-abbreviations option.  Matching the suggestion to a failing source
-// line (rather than firing on any recorded spelling) keeps it from appearing
-// when an unrelated failure occurs in a file that legitimately uses such a
-// spelling as a defined operator on a different line.  Only one suggestion is
-// emitted, anchored at the first such abbreviation in source order.
+// location of every logical abbreviation (.T./.F./.N./.A./.O.) it sees.  For
+// each distinct such spelling that lies on a source line where the parse
+// failed, suggest the -flogical-abbreviations option.  Tying each suggestion to
+// a failing source line keeps it from appearing for a spelling that parses
+// successfully as a defined operator (those fail later in semantics, not here)
+// or on a line that failed for an unrelated reason.
 void Parsing::SuggestLogicalAbbreviations(
     const UserState &userState, Messages &messages) {
   const std::set<CharBlock> &abbreviations{
@@ -329,12 +328,14 @@ void Parsing::SuggestLogicalAbbreviations(
   if (errorLines.empty()) {
     return;
   }
+  // Iterating the std::set visits each distinct abbreviation once, in
+  // increasing source order, so a note is emitted for every distinct .T./.F.
+  // usage on a failing line and the notes appear in a stable order.
   for (const CharBlock &abbreviation : abbreviations) {
     if (auto pos{allCooked_.GetSourcePositionRange(abbreviation)}) {
       if (errorLines.count({&*pos->first.sourceFile, pos->first.line}) > 0) {
         messages.Say(abbreviation,
             "This nonstandard logical abbreviation requires the '-flogical-abbreviations' option"_en_US);
-        return;
       }
     }
   }
diff --git a/flang/test/Parser/logical-abbreviation-suggestion-multiple.f90 b/flang/test/Parser/logical-abbreviation-suggestion-multiple.f90
new file mode 100644
index 0000000000000..83e6d1174516f
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-suggestion-multiple.f90
@@ -0,0 +1,15 @@
+! Every distinct .T./.F. abbreviation that causes a parse failure gets its own
+! suggestion, not just the first one.  The operator form .A. parses as a defined
+! operator and fails later in semantics rather than in the parser, so it gets no
+! parse-time suggestion.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s
+
+logical :: x
+x = .T.
+x = .F.
+x = x .A. x
+end
+
+! CHECK-COUNT-2: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option
+! CHECK-NOT: This nonstandard logical abbreviation

>From 7a40e84687d5ff41d1129c1802f03af399673438 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Fri, 3 Jul 2026 17:57:08 -0400
Subject: [PATCH 3/3] Fixed an issue, where two ocurrences of the same spelling
 were treated as one error

---
 flang/include/flang/Parser/user-state.h       | 17 +++++++++---
 flang/lib/Parser/basic-parsers.h              | 17 ++++++++----
 flang/lib/Parser/parsing.cpp                  | 14 +++++-----
 ...al-abbreviation-defined-operator-mixed.f90 | 26 +++++++++++++++++++
 ...ical-abbreviation-suggestion-duplicate.f90 | 15 +++++++++++
 5 files changed, 74 insertions(+), 15 deletions(-)
 create mode 100644 flang/test/Parser/logical-abbreviation-defined-operator-mixed.f90
 create mode 100644 flang/test/Parser/logical-abbreviation-suggestion-duplicate.f90

diff --git a/flang/include/flang/Parser/user-state.h b/flang/include/flang/Parser/user-state.h
index 056e1fccd8059..6b8bc5d7af7e9 100644
--- a/flang/include/flang/Parser/user-state.h
+++ b/flang/include/flang/Parser/user-state.h
@@ -20,6 +20,7 @@
 #include "flang/Support/Fortran-features.h"
 #include "llvm/Support/raw_ostream.h"
 #include <cinttypes>
+#include <functional>
 #include <optional>
 #include <set>
 #include <unordered_map>
@@ -93,11 +94,21 @@ class UserState {
   // source location of any logical abbreviation (.T./.F./.N./.A./.O.) it
   // encounters, so that a parse failure on that same source line can suggest
   // enabling the -flogical-abbreviations option.
+  // CharBlock's own relational operators compare contents, not positions, so
+  // the set is ordered by position in the cooked character stream: distinct
+  // occurrences of the same spelling stay distinct, while backtracking
+  // re-attempts at the same position collapse into one entry.
+  struct CharBlockByPosition {
+    bool operator()(const CharBlock &x, const CharBlock &y) const {
+      return std::less<const char *>{}(x.begin(), y.begin());
+    }
+  };
   void NoteDisabledLogicalAbbreviation(CharBlock at) {
     disabledLogicalAbbreviations_.insert(at);
   }
-  // Recorded in increasing source order (std::set ordered by CharBlock).
-  const std::set<CharBlock> &disabledLogicalAbbreviations() const {
+  // Recorded in increasing source order.
+  const std::set<CharBlock, CharBlockByPosition> &
+  disabledLogicalAbbreviations() const {
     return disabledLogicalAbbreviations_;
   }
 
@@ -113,7 +124,7 @@ class UserState {
   int nonlabelDoConstructNestingDepth_{0};
 
   std::set<CharBlock> oldStructureComponents_;
-  std::set<CharBlock> disabledLogicalAbbreviations_;
+  std::set<CharBlock, CharBlockByPosition> disabledLogicalAbbreviations_;
 
   common::LanguageFeatureControl features_;
 };
diff --git a/flang/lib/Parser/basic-parsers.h b/flang/lib/Parser/basic-parsers.h
index 800fb74b8a023..d97b7196e37c3 100644
--- a/flang/lib/Parser/basic-parsers.h
+++ b/flang/lib/Parser/basic-parsers.h
@@ -855,17 +855,24 @@ template <LanguageFeature LF, typename PA> class NonstandardParser {
           // Every such abbreviation begins with '.', so only attempt the
           // speculative parse (which copies the parse state) when the next
           // non-blank character could start one.
-          const char *at{state.GetLocation()};
-          const char *p{at};
-          const char *limit{at + state.BytesRemaining()};
+          const char *p{state.GetLocation()};
+          const char *limit{p + state.BytesRemaining()};
           while (p < limit && *p == ' ') {
             ++p;
           }
           if (p < limit && *p == '.') {
             ParseState fork{state};
             if (parser_.Parse(fork)) {
-              ustate->NoteDisabledLogicalAbbreviation(
-                  CharBlock{at, std::max(fork.GetLocation(), at + 1)});
+              // Anchor the recorded span at the first non-blank character so
+              // that re-attempts at the same occurrence from different
+              // productions record the same position, and trim any trailing
+              // blanks the token parser consumed so the span covers exactly
+              // the abbreviation.
+              const char *end{std::max(fork.GetLocation(), p + 1)};
+              while (end > p + 1 && end[-1] == ' ') {
+                --end;
+              }
+              ustate->NoteDisabledLogicalAbbreviation(CharBlock{p, end});
             }
           }
         }
diff --git a/flang/lib/Parser/parsing.cpp b/flang/lib/Parser/parsing.cpp
index 12ca0009e4700..abb02437c672a 100644
--- a/flang/lib/Parser/parsing.cpp
+++ b/flang/lib/Parser/parsing.cpp
@@ -302,14 +302,14 @@ void Parsing::Parse(llvm::raw_ostream &out) {
 
 // When the LogicalAbbreviations feature is disabled, the parser records the
 // location of every logical abbreviation (.T./.F./.N./.A./.O.) it sees.  For
-// each distinct such spelling that lies on a source line where the parse
-// failed, suggest the -flogical-abbreviations option.  Tying each suggestion to
-// a failing source line keeps it from appearing for a spelling that parses
+// each such occurrence that lies on a source line where the parse failed,
+// suggest the -flogical-abbreviations option.  Tying each suggestion to
+// a failing source line keeps it from appearing for an occurrence that parses
 // successfully as a defined operator (those fail later in semantics, not here)
 // or on a line that failed for an unrelated reason.
 void Parsing::SuggestLogicalAbbreviations(
     const UserState &userState, Messages &messages) {
-  const std::set<CharBlock> &abbreviations{
+  const std::set<CharBlock, UserState::CharBlockByPosition> &abbreviations{
       userState.disabledLogicalAbbreviations()};
   if (abbreviations.empty()) {
     return;
@@ -328,9 +328,9 @@ void Parsing::SuggestLogicalAbbreviations(
   if (errorLines.empty()) {
     return;
   }
-  // Iterating the std::set visits each distinct abbreviation once, in
-  // increasing source order, so a note is emitted for every distinct .T./.F.
-  // usage on a failing line and the notes appear in a stable order.
+  // The set is keyed by source position, so iterating it visits each
+  // occurrence once, in increasing source order: a note is emitted for every
+  // .T./.F. usage on a failing line, even when spellings repeat.
   for (const CharBlock &abbreviation : abbreviations) {
     if (auto pos{allCooked_.GetSourcePositionRange(abbreviation)}) {
       if (errorLines.count({&*pos->first.sourceFile, pos->first.line}) > 0) {
diff --git a/flang/test/Parser/logical-abbreviation-defined-operator-mixed.f90 b/flang/test/Parser/logical-abbreviation-defined-operator-mixed.f90
new file mode 100644
index 0000000000000..97cd9b05b5baf
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-defined-operator-mixed.f90
@@ -0,0 +1,26 @@
+! A valid defined-operator use of .f. must not swallow the suggestion for a
+! later parse failure on the same spelling: the suggestion is emitted at the
+! failing occurrence only, not at the valid one.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s --implicit-check-not='This nonstandard logical abbreviation'
+
+module m
+  interface operator(.f.)
+    module procedure neg
+  end interface
+contains
+  pure integer function neg(a)
+    integer, intent(in) :: a
+    neg = -a
+  end function
+end module
+program p
+  use m
+  integer :: r
+  logical :: z
+  r = .f.(4)
+  z = .f.
+end program
+
+! CHECK: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option
+! CHECK-NEXT: z = .f.
diff --git a/flang/test/Parser/logical-abbreviation-suggestion-duplicate.f90 b/flang/test/Parser/logical-abbreviation-suggestion-duplicate.f90
new file mode 100644
index 0000000000000..9b1938fde3d55
--- /dev/null
+++ b/flang/test/Parser/logical-abbreviation-suggestion-duplicate.f90
@@ -0,0 +1,15 @@
+! Each failing occurrence of a logical abbreviation gets its own suggestion,
+! even when the spellings are identical: the recorded occurrences are keyed by
+! source position, not by spelled text.
+
+! RUN: not %flang_fc1 -fsyntax-only %s 2>&1 | FileCheck %s --implicit-check-not='This nonstandard logical abbreviation'
+
+logical :: x, y
+x = .F.
+y = .F.
+end
+
+! CHECK: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option
+! CHECK-NEXT: x = .F.
+! CHECK: This nonstandard logical abbreviation requires the '-flogical-abbreviations' option
+! CHECK-NEXT: y = .F.



More information about the flang-commits mailing list