Skip to content

[flang] Handle "defined" in macro expansions #114844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 5, 2024
Merged

Conversation

klausler
Copy link
Contributor

@klausler klausler commented Nov 4, 2024

The preprocessor implements "defined(X)" and "defined X" in if/elif directive expressions in such a way that they only work at the top level, not when they appear in macro expansions. Fix that, which is a little tricky due to the need to detect the "defined" keyword before applying any macro expansion to its argument, and add a bunch of tests.

Fixes #114064.

The preprocessor implements "defined(X)" and "defined X" in
if/elif directive expressions in such a way that they only
work at the top level, not when they appear in macro expansions.
Fix that, which is a little tricky due to the need to detect
the "defined" keyword before applying any macro expansion to
its argument, and add a bunch of tests.

Fixes llvm#114064.
@klausler klausler requested a review from psteinfeld November 4, 2024 18:04
@llvmbot llvmbot added flang Flang issues not falling into any other category flang:parser labels Nov 4, 2024
@llvmbot
Copy link
Member

llvmbot commented Nov 4, 2024

@llvm/pr-subscribers-flang-parser

Author: Peter Klausler (klausler)

Changes

The preprocessor implements "defined(X)" and "defined X" in if/elif directive expressions in such a way that they only work at the top level, not when they appear in macro expansions. Fix that, which is a little tricky due to the need to detect the "defined" keyword before applying any macro expansion to its argument, and add a bunch of tests.

Fixes #114064.


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

5 Files Affected:

  • (modified) flang/include/flang/Parser/preprocessor.h (+6-3)
  • (modified) flang/include/flang/Parser/token-sequence.h (+1)
  • (modified) flang/lib/Parser/preprocessor.cpp (+61-55)
  • (modified) flang/lib/Parser/token-sequence.cpp (+10)
  • (added) flang/test/Preprocessing/defined-in-macro.F90 (+117)
diff --git a/flang/include/flang/Parser/preprocessor.h b/flang/include/flang/Parser/preprocessor.h
index 57690dd226f628..f8d33460659814 100644
--- a/flang/include/flang/Parser/preprocessor.h
+++ b/flang/include/flang/Parser/preprocessor.h
@@ -48,7 +48,8 @@ class Definition {
 
   bool set_isDisabled(bool disable);
 
-  TokenSequence Apply(const std::vector<TokenSequence> &args, Prescanner &);
+  TokenSequence Apply(const std::vector<TokenSequence> &args, Prescanner &,
+      bool inIfExpression = false);
 
   void Print(llvm::raw_ostream &out, const char *macroName = "") const;
 
@@ -93,7 +94,8 @@ class Preprocessor {
   // behavior.
   std::optional<TokenSequence> MacroReplacement(const TokenSequence &,
       Prescanner &,
-      std::optional<std::size_t> *partialFunctionLikeMacro = nullptr);
+      std::optional<std::size_t> *partialFunctionLikeMacro = nullptr,
+      bool inIfExpression = false);
 
   // Implements a preprocessor directive.
   void Directive(const TokenSequence &, Prescanner &);
@@ -106,7 +108,8 @@ class Preprocessor {
 
   CharBlock SaveTokenAsName(const CharBlock &);
   TokenSequence ReplaceMacros(const TokenSequence &, Prescanner &,
-      std::optional<std::size_t> *partialFunctionLikeMacro = nullptr);
+      std::optional<std::size_t> *partialFunctionLikeMacro = nullptr,
+      bool inIfExpression = false);
   void SkipDisabledConditionalCode(
       const std::string &, IsElseActive, Prescanner &, ProvenanceRange);
   bool IsIfPredicateTrue(const TokenSequence &expr, std::size_t first,
diff --git a/flang/include/flang/Parser/token-sequence.h b/flang/include/flang/Parser/token-sequence.h
index 1f82a3c1a203ac..047c0bed00762a 100644
--- a/flang/include/flang/Parser/token-sequence.h
+++ b/flang/include/flang/Parser/token-sequence.h
@@ -79,6 +79,7 @@ class TokenSequence {
   }
 
   std::size_t SkipBlanks(std::size_t) const;
+  std::optional<std::size_t> SkipBlanksBackwards(std::size_t) const;
 
   // True if anything remains in the sequence at & after the given offset
   // except blanks and line-ending C++ and Fortran free-form comments.
diff --git a/flang/lib/Parser/preprocessor.cpp b/flang/lib/Parser/preprocessor.cpp
index f9b8716941b3bc..6c257f57c2d57c 100644
--- a/flang/lib/Parser/preprocessor.cpp
+++ b/flang/lib/Parser/preprocessor.cpp
@@ -177,8 +177,13 @@ static TokenSequence TokenPasting(TokenSequence &&text) {
   return result;
 }
 
-TokenSequence Definition::Apply(
-    const std::vector<TokenSequence> &args, Prescanner &prescanner) {
+constexpr bool IsDefinedKeyword(CharBlock token) {
+  return token.size() == 7 && (token[0] == 'd' || token[0] == 'D') &&
+      ToLowerCaseLetters(token.ToString()) == "defined";
+}
+
+TokenSequence Definition::Apply(const std::vector<TokenSequence> &args,
+    Prescanner &prescanner, bool inIfExpression) {
   TokenSequence result;
   bool skipping{false};
   int parenthesesNesting{0};
@@ -223,13 +228,16 @@ TokenSequence Definition::Apply(
         const TokenSequence *arg{&args[index]};
         std::optional<TokenSequence> replaced;
         // Don't replace macros in the actual argument if it is preceded or
-        // followed by the token-pasting operator ## in the replacement text.
-        if (prev == 0 || !IsTokenPasting(replacement_.TokenAt(prev - 1))) {
+        // followed by the token-pasting operator ## in the replacement text,
+        // or if we have to worry about "defined(X)"/"defined X" in an
+        // #if/#elif expression.
+        if (!inIfExpression &&
+            (prev == 0 || !IsTokenPasting(replacement_.TokenAt(prev - 1)))) {
           auto next{replacement_.SkipBlanks(j + 1)};
           if (next >= tokens || !IsTokenPasting(replacement_.TokenAt(next))) {
             // Apply macro replacement to the actual argument
-            replaced =
-                prescanner.preprocessor().MacroReplacement(*arg, prescanner);
+            replaced = prescanner.preprocessor().MacroReplacement(
+                *arg, prescanner, nullptr, inIfExpression);
             if (replaced) {
               arg = &*replaced;
             }
@@ -301,7 +309,7 @@ void Preprocessor::Undefine(std::string macro) { definitions_.erase(macro); }
 
 std::optional<TokenSequence> Preprocessor::MacroReplacement(
     const TokenSequence &input, Prescanner &prescanner,
-    std::optional<std::size_t> *partialFunctionLikeMacro) {
+    std::optional<std::size_t> *partialFunctionLikeMacro, bool inIfExpression) {
   // Do quick scan for any use of a defined name.
   if (definitions_.empty()) {
     return std::nullopt;
@@ -311,7 +319,7 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
   for (; j < tokens; ++j) {
     CharBlock token{input.TokenAt(j)};
     if (!token.empty() && IsLegalIdentifierStart(token[0]) &&
-        IsNameDefined(token)) {
+        (IsNameDefined(token) || (inIfExpression && IsDefinedKeyword(token)))) {
       break;
     }
   }
@@ -326,8 +334,8 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
   // replacement text and attempt to proceed.  Otherwise, return, so that
   // the caller may try again with remaining tokens in its input.
   auto CompleteFunctionLikeMacro{
-      [this, &input, &prescanner, &result, &partialFunctionLikeMacro](
-          std::size_t after, const TokenSequence &replacement,
+      [this, &input, &prescanner, &result, &partialFunctionLikeMacro,
+          inIfExpression](std::size_t after, const TokenSequence &replacement,
           std::size_t pFLMOffset) {
         if (after < input.SizeInTokens()) {
           result.Put(replacement, 0, pFLMOffset);
@@ -335,8 +343,8 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
           suffix.Put(
               replacement, pFLMOffset, replacement.SizeInTokens() - pFLMOffset);
           suffix.Put(input, after, input.SizeInTokens() - after);
-          auto further{
-              ReplaceMacros(suffix, prescanner, partialFunctionLikeMacro)};
+          auto further{ReplaceMacros(
+              suffix, prescanner, partialFunctionLikeMacro, inIfExpression)};
           if (partialFunctionLikeMacro && *partialFunctionLikeMacro) {
             // still not closed
             **partialFunctionLikeMacro += result.SizeInTokens();
@@ -357,7 +365,28 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
       result.Put(input, j);
       continue;
     }
+    // Process identifier in replacement text.
     auto it{definitions_.find(token)};
+    // Is in the X in "defined(X)" or "defined X" in an #if/#elif expression?
+    if (inIfExpression) {
+      if (auto prev{result.SkipBlanksBackwards(result.SizeInTokens())}) {
+        bool ok{true};
+        std::optional<std::size_t> rightParenthesis;
+        if (result.TokenAt(*prev).OnlyNonBlank() == '(') {
+          prev = result.SkipBlanksBackwards(*prev);
+          rightParenthesis = input.SkipBlanks(j + 1);
+          ok = *rightParenthesis < tokens &&
+              input.TokenAt(*rightParenthesis).OnlyNonBlank() == ')';
+        }
+        if (ok && prev && IsDefinedKeyword(result.TokenAt(*prev))) {
+          result = TokenSequence{result, 0, *prev}; // trims off "defined ("
+          char truth{it != definitions_.end() ? '1' : '0'};
+          result.Put(&truth, 1, allSources_.CompilerInsertionProvenance(truth));
+          j = rightParenthesis.value_or(j);
+          continue;
+        }
+      }
+    }
     if (it == definitions_.end()) {
       result.Put(input, j);
       continue;
@@ -403,8 +432,8 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
       }
       std::optional<std::size_t> partialFLM;
       def->set_isDisabled(true);
-      TokenSequence replaced{TokenPasting(
-          ReplaceMacros(def->replacement(), prescanner, &partialFLM))};
+      TokenSequence replaced{TokenPasting(ReplaceMacros(
+          def->replacement(), prescanner, &partialFLM, inIfExpression))};
       def->set_isDisabled(false);
       if (partialFLM &&
           CompleteFunctionLikeMacro(j + 1, replaced, *partialFLM)) {
@@ -476,11 +505,11 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
             (n + 1 == argStart.size() ? k : argStart[n + 1] - 1) - at};
         args.emplace_back(TokenSequence(input, at, count));
       }
-      TokenSequence applied{def->Apply(args, prescanner)};
+      TokenSequence applied{def->Apply(args, prescanner, inIfExpression)};
       std::optional<std::size_t> partialFLM;
       def->set_isDisabled(true);
-      TokenSequence replaced{
-          ReplaceMacros(std::move(applied), prescanner, &partialFLM)};
+      TokenSequence replaced{ReplaceMacros(
+          std::move(applied), prescanner, &partialFLM, inIfExpression)};
       def->set_isDisabled(false);
       if (partialFLM &&
           CompleteFunctionLikeMacro(k + 1, replaced, *partialFLM)) {
@@ -501,9 +530,9 @@ std::optional<TokenSequence> Preprocessor::MacroReplacement(
 
 TokenSequence Preprocessor::ReplaceMacros(const TokenSequence &tokens,
     Prescanner &prescanner,
-    std::optional<std::size_t> *partialFunctionLikeMacro) {
-  if (std::optional<TokenSequence> repl{
-          MacroReplacement(tokens, prescanner, partialFunctionLikeMacro)}) {
+    std::optional<std::size_t> *partialFunctionLikeMacro, bool inIfExpression) {
+  if (std::optional<TokenSequence> repl{MacroReplacement(
+          tokens, prescanner, partialFunctionLikeMacro, inIfExpression)}) {
     return std::move(*repl);
   }
   return tokens;
@@ -1215,50 +1244,27 @@ static std::int64_t ExpressionValue(const TokenSequence &token,
   return left;
 }
 
-bool Preprocessor::IsIfPredicateTrue(const TokenSequence &expr,
+bool Preprocessor::IsIfPredicateTrue(const TokenSequence &directive,
     std::size_t first, std::size_t exprTokens, Prescanner &prescanner) {
-  TokenSequence expr1{expr, first, exprTokens};
-  if (expr1.HasBlanks()) {
-    expr1.RemoveBlanks();
-  }
-  TokenSequence expr2;
-  for (std::size_t j{0}; j < expr1.SizeInTokens(); ++j) {
-    if (ToLowerCaseLetters(expr1.TokenAt(j).ToString()) == "defined") {
-      CharBlock name;
-      if (j + 3 < expr1.SizeInTokens() &&
-          expr1.TokenAt(j + 1).OnlyNonBlank() == '(' &&
-          expr1.TokenAt(j + 3).OnlyNonBlank() == ')') {
-        name = expr1.TokenAt(j + 2);
-        j += 3;
-      } else if (j + 1 < expr1.SizeInTokens() &&
-          IsLegalIdentifierStart(expr1.TokenAt(j + 1))) {
-        name = expr1.TokenAt(++j);
-      }
-      if (!name.empty()) {
-        char truth{IsNameDefined(name) ? '1' : '0'};
-        expr2.Put(&truth, 1, allSources_.CompilerInsertionProvenance(truth));
-        continue;
-      }
-    }
-    expr2.Put(expr1, j);
-  }
-  TokenSequence expr3{ReplaceMacros(expr2, prescanner)};
-  if (expr3.HasBlanks()) {
-    expr3.RemoveBlanks();
+  TokenSequence expr{directive, first, exprTokens};
+  TokenSequence replaced{
+      ReplaceMacros(expr, prescanner, nullptr, /*inIfExpression=*/true)};
+  if (replaced.HasBlanks()) {
+    replaced.RemoveBlanks();
   }
-  if (expr3.empty()) {
+  if (replaced.empty()) {
     prescanner.Say(expr.GetProvenanceRange(), "empty expression"_err_en_US);
     return false;
   }
   std::size_t atToken{0};
   std::optional<Message> error;
-  bool result{ExpressionValue(expr3, 0, &atToken, &error) != 0};
+  bool result{ExpressionValue(replaced, 0, &atToken, &error) != 0};
   if (error) {
     prescanner.Say(std::move(*error));
-  } else if (atToken < expr3.SizeInTokens() &&
-      expr3.TokenAt(atToken).ToString() != "!") {
-    prescanner.Say(expr3.GetIntervalProvenanceRange(
-                       atToken, expr3.SizeInTokens() - atToken),
+  } else if (atToken < replaced.SizeInTokens() &&
+      replaced.TokenAt(atToken).ToString() != "!") {
+    prescanner.Say(replaced.GetIntervalProvenanceRange(
+                       atToken, replaced.SizeInTokens() - atToken),
         atToken == 0 ? "could not parse any expression"_err_en_US
                      : "excess characters after expression"_err_en_US);
   }
diff --git a/flang/lib/Parser/token-sequence.cpp b/flang/lib/Parser/token-sequence.cpp
index c73bca1df33de3..cdbe89b1eb441c 100644
--- a/flang/lib/Parser/token-sequence.cpp
+++ b/flang/lib/Parser/token-sequence.cpp
@@ -61,6 +61,16 @@ std::size_t TokenSequence::SkipBlanks(std::size_t at) const {
   return tokens; // even if at > tokens
 }
 
+std::optional<std::size_t> TokenSequence::SkipBlanksBackwards(
+    std::size_t at) const {
+  while (at-- > 0) {
+    if (!TokenAt(at).IsBlank()) {
+      return at;
+    }
+  }
+  return std::nullopt;
+}
+
 // C-style /*comments*/ are removed from preprocessing directive
 // token sequences by the prescanner, but not C++ or Fortran
 // free-form line-ending comments (//...  and !...) because
diff --git a/flang/test/Preprocessing/defined-in-macro.F90 b/flang/test/Preprocessing/defined-in-macro.F90
new file mode 100644
index 00000000000000..9416b9c81b5523
--- /dev/null
+++ b/flang/test/Preprocessing/defined-in-macro.F90
@@ -0,0 +1,117 @@
+! RUN: %flang -E %s 2>&1 | FileCheck %s
+
+! CHECK: print *, 'pass 1'
+#define IS_DEFINED
+#define M1 defined(IS_DEFINED)
+#if M1
+print *, 'pass 1'
+#else
+print *, 'fail 1'
+#endif
+
+! CHECK: print *, 'pass 2'
+#define M2 defined IS_DEFINED
+#if M2
+print *, 'pass 2'
+#else
+print *, 'fail 2'
+#endif
+
+! CHECK: print *, 'pass 3'
+#define M3 defined(IS_UNDEFINED)
+#if M3
+print *, 'fail 3'
+#else
+print *, 'pass 3'
+#endif
+
+! CHECK: print *, 'pass 4'
+#define M4 defined IS_UNDEFINED
+#if M4
+print *, 'fail 4'
+#else
+print *, 'pass 4'
+#endif
+
+! CHECK: print *, 'pass 5'
+#define DEFINED_KEYWORD defined
+#define M5(x) DEFINED_KEYWORD(x)
+#define KWM1 1
+#if M5(KWM1)
+print *, 'pass 5'
+#else
+print *, 'fail 5'
+#endif
+
+! CHECK: print *, 'pass 6'
+#define KWM2 KWM1
+#if M5(KWM2)
+print *, 'pass 6'
+#else
+print *, 'fail 6'
+#endif
+
+! CHECK: print *, 'pass 7'
+#if M5(IS_UNDEFINED)
+print *, 'fail 7'
+#else
+print *, 'pass 7'
+#endif
+
+! CHECK: print *, 'pass 8'
+#define KWM3 IS_UNDEFINED
+#if M5(KWM3)
+print *, 'pass 8'
+#else
+print *, 'fail 8'
+#endif
+
+! CHECK: print *, 'pass 9'
+#define M6(x) defined(x)
+#if M6(KWM1)
+print *, 'pass 9'
+#else
+print *, 'fail 9'
+#endif
+
+! CHECK: print *, 'pass 10'
+#if M6(KWM2)
+print *, 'pass 10'
+#else
+print *, 'fail 10'
+#endif
+
+! CHECK: print *, 'pass 11'
+#if M6(IS_UNDEFINED)
+print *, 'fail 11'
+#else
+print *, 'pass 11'
+#endif
+
+! CHECK: print *, 'pass 12'
+#if M6(KWM3)
+print *, 'pass 12'
+#else
+print *, 'fail 12'
+#endif
+
+! CHECK: print *, 'pass 13'
+#define M7(A, B) ((A) * 10000 + (B) * 100)
+#define M8(A, B, C, AA, BB) ( \
+  (defined(AA) && defined(BB)) && \
+  (M7(A, B) C M7(AA, BB)))
+#if M8(9, 5, >, BAZ, FUX)
+print *, 'fail 13'
+#else
+print *, 'pass 13'
+#endif
+
+! CHECK: print *, 'pass 14'
+#define M9() (defined(IS_UNDEFINED))
+#if M9()
+print *, 'fail 14'
+#else
+print *, 'pass 14'
+#endif
+
+end

Copy link
Contributor

@psteinfeld psteinfeld left a comment

Choose a reason for hiding this comment

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

All builds and tests correctly and looks good.

@klausler klausler merged commit 850d42f into llvm:main Nov 5, 2024
11 checks passed
@klausler klausler deleted the bug114064 branch November 5, 2024 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
flang:parser flang Flang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[flang] Flang fails to scan a Fortran file with preprocessor macros
3 participants