[llvm] [FuncAttrs][LTO] Relax norecurse attribute inference during postlink LTO (PR #158608)

Usha Gupta via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 29 08:48:44 PDT 2025


https://github.com/usha1830 updated https://github.com/llvm/llvm-project/pull/158608

>From 09220c5fb0ae2ab6d90dddb6b24e85dd7be8fd86 Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Mon, 15 Sep 2025 10:52:48 +0000
Subject: [PATCH 1/3] [FuncAttrs][LTO] Relax norecurse attribute inference
 during postlink LTO

---
 .../llvm/Transforms/IPO/FunctionAttrs.h       |  13 ++
 llvm/lib/Passes/PassBuilderPipelines.cpp      |   3 +-
 llvm/lib/Passes/PassRegistry.def              |   1 +
 llvm/lib/Transforms/IPO/FunctionAttrs.cpp     | 123 +++++++++++++---
 llvm/test/Other/new-pm-lto-defaults.ll        |   1 +
 .../norecurse_libfunc_address_taken.ll        |  34 +++++
 .../norecurse_libfunc_no_address_taken.ll     |  39 +++++
 .../norecurse_multi_scc_indirect_recursion.ll | 138 ++++++++++++++++++
 ...norecurse_multi_scc_indirect_recursion1.ll |  95 ++++++++++++
 .../norecurse_self_recursive_callee.ll        | 135 +++++++++++++++++
 10 files changed, 559 insertions(+), 23 deletions(-)
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll

diff --git a/llvm/include/llvm/Transforms/IPO/FunctionAttrs.h b/llvm/include/llvm/Transforms/IPO/FunctionAttrs.h
index 754714dceb7a6..eaca0a8fdac0b 100644
--- a/llvm/include/llvm/Transforms/IPO/FunctionAttrs.h
+++ b/llvm/include/llvm/Transforms/IPO/FunctionAttrs.h
@@ -79,6 +79,19 @@ class ReversePostOrderFunctionAttrsPass
   LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
 };
 
+/// Additional 'norecurse' attribute deduction during postlink LTO phase.
+///
+/// This is a module pass that infers 'norecurse' attribute on functions.
+/// It runs during LTO and analyzes the module's call graph to find functions
+/// that are guaranteed not to call themselves, either directly or indirectly.
+/// The pass uses a module-wide flag which checks if any function's address is
+/// taken or any function in the module has external linkage, to safely handle
+/// indirect and library function calls from current function.
+class NoRecurseLTOInferencePass
+    : public PassInfoMixin<NoRecurseLTOInferencePass> {
+public:
+  LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM);
+};
 } // end namespace llvm
 
 #endif // LLVM_TRANSFORMS_IPO_FUNCTIONATTRS_H
diff --git a/llvm/lib/Passes/PassBuilderPipelines.cpp b/llvm/lib/Passes/PassBuilderPipelines.cpp
index 98821bb1408a7..ca16f2e580e85 100644
--- a/llvm/lib/Passes/PassBuilderPipelines.cpp
+++ b/llvm/lib/Passes/PassBuilderPipelines.cpp
@@ -1944,6 +1944,7 @@ PassBuilder::buildLTODefaultPipeline(OptimizationLevel Level,
   // is fixed.
   MPM.addPass(WholeProgramDevirtPass(ExportSummary, nullptr));
 
+  MPM.addPass(NoRecurseLTOInferencePass());
   // Stop here at -O1.
   if (Level == OptimizationLevel::O1) {
     // The LowerTypeTestsPass needs to run to lower type metadata and the
@@ -2355,4 +2356,4 @@ AAManager PassBuilder::buildDefaultAAPipeline() {
 bool PassBuilder::isInstrumentedPGOUse() const {
   return (PGOOpt && PGOOpt->Action == PGOOptions::IRUse) ||
          !UseCtxProfile.empty();
-}
\ No newline at end of file
+}
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 1d015971dfbdf..2acfd3c1789c8 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -119,6 +119,7 @@ MODULE_PASS("metarenamer", MetaRenamerPass())
 MODULE_PASS("module-inline", ModuleInlinerPass())
 MODULE_PASS("name-anon-globals", NameAnonGlobalPass())
 MODULE_PASS("no-op-module", NoOpModulePass())
+MODULE_PASS("norecurse-lto-inference", NoRecurseLTOInferencePass())
 MODULE_PASS("nsan", NumericalStabilitySanitizerPass())
 MODULE_PASS("openmp-opt", OpenMPOptPass())
 MODULE_PASS("openmp-opt-postlink",
diff --git a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
index 8d9a0e7eaef63..b5f92d8c3cf65 100644
--- a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
@@ -2067,6 +2067,36 @@ static void inferAttrsFromFunctionBodies(const SCCNodeSet &SCCNodes,
   AI.run(SCCNodes, Changed);
 }
 
+// Determines if the function 'F' can be marked 'norecurse'.
+// It returns true if any call within 'F' could lead to a recursive
+// call back to 'F', and false otherwise.
+// The 'AnyFunctionsAddressIsTaken' parameter is a module-wide flag
+// that is true if any function's address is taken, or if any function
+// has external linkage. This is used to determine the safety of
+// external/library calls.
+static bool hasRecursiveCallee(Function &F,
+                               bool AnyFunctionsAddressIsTaken = true) {
+  for (const auto &BB : F) {
+    for (const auto &I : BB.instructionsWithoutDebug()) {
+      if (const auto *CB = dyn_cast<CallBase>(&I)) {
+        const Function *Callee = CB->getCalledFunction();
+        if (!Callee || Callee == &F)
+          return true;
+
+        if (Callee->doesNotRecurse())
+          continue;
+
+        if (!AnyFunctionsAddressIsTaken ||
+            (Callee->isDeclaration() &&
+             Callee->hasFnAttribute(Attribute::NoCallback)))
+          continue;
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
 static void addNoRecurseAttrs(const SCCNodeSet &SCCNodes,
                               SmallPtrSet<Function *, 8> &Changed) {
   // Try and identify functions that do not recurse.
@@ -2078,28 +2108,14 @@ static void addNoRecurseAttrs(const SCCNodeSet &SCCNodes,
   Function *F = *SCCNodes.begin();
   if (!F || !F->hasExactDefinition() || F->doesNotRecurse())
     return;
-
-  // If all of the calls in F are identifiable and are to norecurse functions, F
-  // is norecurse. This check also detects self-recursion as F is not currently
-  // marked norecurse, so any called from F to F will not be marked norecurse.
-  for (auto &BB : *F)
-    for (auto &I : BB.instructionsWithoutDebug())
-      if (auto *CB = dyn_cast<CallBase>(&I)) {
-        Function *Callee = CB->getCalledFunction();
-        if (!Callee || Callee == F ||
-            (!Callee->doesNotRecurse() &&
-             !(Callee->isDeclaration() &&
-               Callee->hasFnAttribute(Attribute::NoCallback))))
-          // Function calls a potentially recursive function.
-          return;
-      }
-
-  // Every call was to a non-recursive function other than this function, and
-  // we have no indirect recursion as the SCC size is one. This function cannot
-  // recurse.
-  F->setDoesNotRecurse();
-  ++NumNoRecurse;
-  Changed.insert(F);
+  if (!hasRecursiveCallee(*F)) {
+    // Every call was to a non-recursive function other than this function, and
+    // we have no indirect recursion as the SCC size is one. This function
+    // cannot recurse.
+    F->setDoesNotRecurse();
+    ++NumNoRecurse;
+    Changed.insert(F);
+  }
 }
 
 // Set the noreturn function attribute if possible.
@@ -2429,3 +2445,66 @@ ReversePostOrderFunctionAttrsPass::run(Module &M, ModuleAnalysisManager &AM) {
   PA.preserve<LazyCallGraphAnalysis>();
   return PA;
 }
+
+PreservedAnalyses NoRecurseLTOInferencePass::run(Module &M,
+                                                 ModuleAnalysisManager &MAM) {
+
+  // Check if any function in the whole program has its address taken or has
+  // potentially external linkage.
+  // We use this information when inferring norecurse attribute: If there is
+  // no function whose address is taken and all functions have internal
+  // linkage, there is no path for a callback to any user function.
+  bool AnyFunctionsAddressIsTaken = false;
+  for (Function &F : M) {
+    if (F.isDeclaration() || F.doesNotRecurse()) {
+      continue;
+    }
+    if (!F.hasLocalLinkage() || F.hasAddressTaken()) {
+      AnyFunctionsAddressIsTaken = true;
+      break;
+    }
+  }
+
+  // Run norecurse inference on all RefSCCs in the LazyCallGraph for this
+  // module.
+  bool Changed = false;
+  LazyCallGraph &CG = MAM.getResult<LazyCallGraphAnalysis>(M);
+  CG.buildRefSCCs();
+
+  for (LazyCallGraph::RefSCC &RC : CG.postorder_ref_sccs()) {
+    // Skip any RefSCC that is part of a call cycle. A RefSCC containing more
+    // than one SCC indicates a recursive relationship, which could involve
+    // direct or indirect calls.
+    if (RC.size() > 1) {
+      continue;
+    }
+
+    // A single-SCC RefSCC could still be a self-loop.
+    LazyCallGraph::SCC &S = *RC.begin();
+    if (S.size() > 1) {
+      continue;
+    }
+
+    // Get the single function from this SCC.
+    Function &F = S.begin()->getFunction();
+    if (!F.hasExactDefinition() || F.doesNotRecurse()) {
+      continue;
+    }
+
+    // If the analysis confirms that this function has no recursive calls
+    // (either direct, indirect, or through external linkages),
+    // we can safely apply the norecurse attribute.
+    if (!hasRecursiveCallee(F, AnyFunctionsAddressIsTaken)) {
+      F.setDoesNotRecurse();
+      ++NumNoRecurse;
+      Changed = true;
+    }
+  }
+
+  PreservedAnalyses PA;
+  if (Changed)
+    PA.preserve<LazyCallGraphAnalysis>();
+  else
+    PA = PreservedAnalyses::all();
+  return PA;
+}
diff --git a/llvm/test/Other/new-pm-lto-defaults.ll b/llvm/test/Other/new-pm-lto-defaults.ll
index 3aea0f2061f3e..f595dfe1d6845 100644
--- a/llvm/test/Other/new-pm-lto-defaults.ll
+++ b/llvm/test/Other/new-pm-lto-defaults.ll
@@ -67,6 +67,7 @@
 ; CHECK-O1-NEXT: Running analysis: TargetLibraryAnalysis
 ; CHECK-O-NEXT: Running pass: GlobalSplitPass
 ; CHECK-O-NEXT: Running pass: WholeProgramDevirtPass
+; CHECK-O-NEXT: Running pass: NoRecurseLTOInferencePass
 ; CHECK-O23SZ-NEXT: Running pass: CoroEarlyPass
 ; CHECK-O1-NEXT: Running pass: LowerTypeTestsPass
 ; CHECK-O23SZ-NEXT: Running pass: GlobalOptPass
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll
new file mode 100644
index 0000000000000..0ec36f8147872
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll
@@ -0,0 +1,34 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
+
+; This test includes a call to a library function which is not marked as
+; NoCallback. Function bob() does not have internal linkage and hence prevents
+; norecurse to be added.
+
+ at .str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
+
+define dso_local void @bob() {
+; CHECK-LABEL: define dso_local void @bob() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+; CHECK-NEXT:    ret void
+;
+entry:
+  %call = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+  ret void
+}
+
+declare noundef i32 @printf(ptr noundef readonly captures(none), ...)
+
+define dso_local noundef i32 @main() norecurse {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bob()
+; CHECK-NEXT:    ret i32 0
+;
+entry:
+  tail call void @bob()
+  ret i32 0
+}
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll
new file mode 100644
index 0000000000000..6d13d5262f9f7
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll
@@ -0,0 +1,39 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
+
+; This test includes a call to a library function which is not marked as
+; NoCallback. All functions except main() are internal and main is marked
+; norecurse, so as to not block norecurse to be added to bob().
+
+ at .str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
+
+; Function Attrs: nofree noinline nounwind uwtable
+define internal void @bob() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @bob(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+; CHECK-NEXT:    ret void
+;
+entry:
+  %call = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+  ret void
+}
+
+; Function Attrs: nofree nounwind
+declare noundef i32 @printf(ptr noundef readonly captures(none), ...)
+
+; Function Attrs: nofree norecurse nounwind uwtable
+define dso_local noundef i32 @main() norecurse {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bob()
+; CHECK-NEXT:    ret i32 0
+;
+entry:
+  tail call void @bob()
+  ret i32 0
+}
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll
new file mode 100644
index 0000000000000..8264cf33df4eb
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll
@@ -0,0 +1,138 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
+
+; This test includes a call graph with multiple SCCs. The purpose of this is
+; to check that norecurse is not added when a function is part of non-singular
+; SCC.
+; There are three different SCCs in this test:
+;  SCC#1:  f1, foo, bar, foo1, bar1
+;  SCC#2:  bar2, bar3, bar4
+;  SCC#3:  baz, fun
+; None of these functions should be marked as norecurse
+
+define internal void @bar1() {
+; CHECK-LABEL: define internal void @bar1() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @f1()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @f1()
+  ret void
+}
+
+define internal void @f1() {
+; CHECK-LABEL: define internal void @f1() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @foo()
+; CHECK-NEXT:    tail call void @bar2()
+; CHECK-NEXT:    tail call void @baz()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @foo()
+  tail call void @bar2()
+  tail call void @baz()
+  ret void
+}
+
+define dso_local noundef i32 @main() norecurse {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @f1()
+; CHECK-NEXT:    ret i32 0
+;
+entry:
+  tail call void @f1()
+  ret i32 0
+}
+
+define internal void @foo1() {
+; CHECK-LABEL: define internal void @foo1() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar1()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar1()
+  ret void
+}
+
+define internal void @bar() {
+; CHECK-LABEL: define internal void @bar() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @foo1()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @foo1()
+  ret void
+}
+
+define internal void @foo() {
+; CHECK-LABEL: define internal void @foo() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar()
+  ret void
+}
+
+define internal void @bar4() {
+; CHECK-LABEL: define internal void @bar4() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar2()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar2()
+  ret void
+}
+
+define internal void @bar2() {
+; CHECK-LABEL: define internal void @bar2() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar3()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar3()
+  ret void
+}
+
+define internal void @bar3() {
+; CHECK-LABEL: define internal void @bar3() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar4()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar4()
+  ret void
+}
+
+define internal void @fun() {
+; CHECK-LABEL: define internal void @fun() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @baz()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @baz()
+  ret void
+}
+
+define internal void @baz() {
+; CHECK-LABEL: define internal void @baz() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @fun()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @fun()
+  ret void
+}
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll
new file mode 100644
index 0000000000000..af986eff3e13c
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll
@@ -0,0 +1,95 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
+
+; This test includes a call graph with multiple SCCs. The purpose of this is
+; to check that norecurse is added to a function which calls a function which
+; is indirectly recursive but is not part of the recursive chain.
+; There are two SCCs in this test:
+;  SCC#1:  bar2, bar3, bar4
+;  SCC#2:  baz, fun
+; f1() calls bar2 and baz, both of which are part of some indirect recursive
+; chain. but does not call back f1() and hence f1() can be marked as
+; norecurse.
+
+define dso_local noundef i32 @main() norecurse {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @f1()
+; CHECK-NEXT:    ret i32 0
+;
+entry:
+  tail call void @f1()
+  ret i32 0
+}
+
+define internal void @f1() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @f1(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar2()
+; CHECK-NEXT:    tail call void @baz()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar2()
+  tail call void @baz()
+  ret void
+}
+
+define internal void @bar4() {
+; CHECK-LABEL: define internal void @bar4() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar2()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar2()
+  ret void
+}
+
+define internal void @bar2() {
+; CHECK-LABEL: define internal void @bar2() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar3()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar3()
+  ret void
+}
+
+define internal void @bar3() {
+; CHECK-LABEL: define internal void @bar3() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bar4()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @bar4()
+  ret void
+}
+
+define internal void @fun() {
+; CHECK-LABEL: define internal void @fun() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @baz()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @baz()
+  ret void
+}
+
+define internal void @baz() {
+; CHECK-LABEL: define internal void @baz() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @fun()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @fun()
+  ret void
+}
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll
new file mode 100644
index 0000000000000..554642ff6963c
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll
@@ -0,0 +1,135 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
+
+; This test includes a call graph with a self recursive function.
+; The purpose of this is to check that norecurse is added to functions
+; which have a self-recursive function in the call-chain.
+; The call-chain in this test is as follows
+; main -> bob -> callee1 -> callee2 -> callee3 -> callee4 -> callee5
+; where callee5 is self recursive.
+
+ at x = dso_local global i32 4, align 4
+ at y = dso_local global i32 2, align 4
+
+define internal void @callee6() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @callee6(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load volatile i32, ptr @y, align 4
+; CHECK-NEXT:    [[INC:%.*]] = add nsw i32 [[TMP0]], 1
+; CHECK-NEXT:    store volatile i32 [[INC]], ptr @y, align 4
+; CHECK-NEXT:    ret void
+;
+entry:
+  %0 = load volatile i32, ptr @y, align 4
+  %inc = add nsw i32 %0, 1
+  store volatile i32 %inc, ptr @y, align 4
+  ret void
+}
+
+define internal void @callee5(i32 noundef %x) {
+; CHECK-LABEL: define internal void @callee5(
+; CHECK-SAME: i32 noundef [[X:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[X]], 0
+; CHECK-NEXT:    br i1 [[CMP]], label %[[IF_THEN:.*]], label %[[IF_END:.*]]
+; CHECK:       [[IF_THEN]]:
+; CHECK-NEXT:    tail call void @callee5(i32 noundef [[X]])
+; CHECK-NEXT:    br label %[[IF_END]]
+; CHECK:       [[IF_END]]:
+; CHECK-NEXT:    tail call void @callee6()
+; CHECK-NEXT:    ret void
+;
+entry:
+  %cmp = icmp sgt i32 %x, 0
+  br i1 %cmp, label %if.then, label %if.end
+
+if.then:                                          ; preds = %entry
+  tail call void @callee5(i32 noundef %x)
+  br label %if.end
+
+if.end:                                           ; preds = %if.then, %entry
+  tail call void @callee6()
+  ret void
+}
+
+define internal void @callee4() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @callee4(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load volatile i32, ptr @x, align 4
+; CHECK-NEXT:    tail call void @callee5(i32 noundef [[TMP0]])
+; CHECK-NEXT:    ret void
+;
+entry:
+  %0 = load volatile i32, ptr @x, align 4
+  tail call void @callee5(i32 noundef %0)
+  ret void
+}
+
+define internal void @callee3() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @callee3(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @callee4()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @callee4()
+  ret void
+}
+
+define internal void @callee2() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @callee2(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @callee3()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @callee3()
+  ret void
+}
+
+define internal void @callee1() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @callee1(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @callee2()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @callee2()
+  ret void
+}
+
+define internal void @bob() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @bob(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @callee1()
+; CHECK-NEXT:    ret void
+;
+entry:
+  tail call void @callee1()
+  ret void
+}
+
+define dso_local noundef i32 @main() norecurse {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    tail call void @bob()
+; CHECK-NEXT:    ret i32 0
+;
+entry:
+  tail call void @bob()
+  ret i32 0
+}

>From d7127b6c7578e1e3ba80d41fc5a97cdb910d6982 Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Thu, 18 Sep 2025 14:48:34 +0000
Subject: [PATCH 2/3] Update test files as per comments

---
 llvm/lib/Transforms/IPO/FunctionAttrs.cpp     | 20 ++--
 .../norecurse_libfunc_address_taken.ll        | 18 ++--
 .../norecurse_libfunc_no_address_taken.ll     | 18 ++--
 .../norecurse_multi_scc_indirect_recursion.ll |  9 +-
 ...norecurse_multi_scc_indirect_recursion1.ll |  9 +-
 .../norecurse_self_recursive_callee.ll        | 97 +++++--------------
 6 files changed, 69 insertions(+), 102 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
index b5f92d8c3cf65..ccf2fafb4493b 100644
--- a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
@@ -2074,8 +2074,8 @@ static void inferAttrsFromFunctionBodies(const SCCNodeSet &SCCNodes,
 // that is true if any function's address is taken, or if any function
 // has external linkage. This is used to determine the safety of
 // external/library calls.
-static bool hasRecursiveCallee(Function &F,
-                               bool AnyFunctionsAddressIsTaken = true) {
+static bool mayHaveRecursiveCallee(Function &F,
+                                   bool AnyFunctionsAddressIsTaken = true) {
   for (const auto &BB : F) {
     for (const auto &I : BB.instructionsWithoutDebug()) {
       if (const auto *CB = dyn_cast<CallBase>(&I)) {
@@ -2108,7 +2108,7 @@ static void addNoRecurseAttrs(const SCCNodeSet &SCCNodes,
   Function *F = *SCCNodes.begin();
   if (!F || !F->hasExactDefinition() || F->doesNotRecurse())
     return;
-  if (!hasRecursiveCallee(*F)) {
+  if (!mayHaveRecursiveCallee(*F)) {
     // Every call was to a non-recursive function other than this function, and
     // we have no indirect recursion as the SCC size is one. This function
     // cannot recurse.
@@ -2456,9 +2456,8 @@ PreservedAnalyses NoRecurseLTOInferencePass::run(Module &M,
   // linkage, there is no path for a callback to any user function.
   bool AnyFunctionsAddressIsTaken = false;
   for (Function &F : M) {
-    if (F.isDeclaration() || F.doesNotRecurse()) {
+    if (F.isDeclaration() || F.doesNotRecurse())
       continue;
-    }
     if (!F.hasLocalLinkage() || F.hasAddressTaken()) {
       AnyFunctionsAddressIsTaken = true;
       break;
@@ -2475,26 +2474,23 @@ PreservedAnalyses NoRecurseLTOInferencePass::run(Module &M,
     // Skip any RefSCC that is part of a call cycle. A RefSCC containing more
     // than one SCC indicates a recursive relationship, which could involve
     // direct or indirect calls.
-    if (RC.size() > 1) {
+    if (RC.size() > 1)
       continue;
-    }
 
     // A single-SCC RefSCC could still be a self-loop.
     LazyCallGraph::SCC &S = *RC.begin();
-    if (S.size() > 1) {
+    if (S.size() > 1)
       continue;
-    }
 
     // Get the single function from this SCC.
     Function &F = S.begin()->getFunction();
-    if (!F.hasExactDefinition() || F.doesNotRecurse()) {
+    if (!F.hasExactDefinition() || F.doesNotRecurse())
       continue;
-    }
 
     // If the analysis confirms that this function has no recursive calls
     // (either direct, indirect, or through external linkages),
     // we can safely apply the norecurse attribute.
-    if (!hasRecursiveCallee(F, AnyFunctionsAddressIsTaken)) {
+    if (!mayHaveRecursiveCallee(F, AnyFunctionsAddressIsTaken)) {
       F.setDoesNotRecurse();
       ++NumNoRecurse;
       Changed = true;
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll
index 0ec36f8147872..bcdf75b021866 100644
--- a/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_address_taken.ll
@@ -1,4 +1,4 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
 ; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
 
 ; This test includes a call to a library function which is not marked as
@@ -7,22 +7,25 @@
 
 @.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
 
+;.
+; CHECK: @.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
+;.
 define dso_local void @bob() {
 ; CHECK-LABEL: define dso_local void @bob() {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+; CHECK-NEXT:    [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @.str)
 ; CHECK-NEXT:    ret void
 ;
 entry:
-  %call = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+  %call = tail call i32 (ptr, ...) @printf(ptr  nonnull dereferenceable(1) @.str)
   ret void
 }
 
-declare noundef i32 @printf(ptr noundef readonly captures(none), ...)
+declare  i32 @printf(ptr  readonly captures(none), ...)
 
-define dso_local noundef i32 @main() norecurse {
+define dso_local  i32 @main() norecurse {
 ; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-LABEL: define dso_local i32 @main(
 ; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    tail call void @bob()
@@ -32,3 +35,6 @@ entry:
   tail call void @bob()
   ret i32 0
 }
+;.
+; CHECK: attributes #[[ATTR0]] = { norecurse }
+;.
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll
index 6d13d5262f9f7..a03b4ca635b1e 100644
--- a/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_libfunc_no_address_taken.ll
@@ -1,4 +1,4 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
 ; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
 
 ; This test includes a call to a library function which is not marked as
@@ -8,26 +8,29 @@
 @.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
 
 ; Function Attrs: nofree noinline nounwind uwtable
+;.
+; CHECK: @.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
+;.
 define internal void @bob() {
 ; CHECK: Function Attrs: norecurse
 ; CHECK-LABEL: define internal void @bob(
 ; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+; CHECK-NEXT:    [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @.str)
 ; CHECK-NEXT:    ret void
 ;
 entry:
-  %call = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
+  %call = tail call i32 (ptr, ...) @printf(ptr  nonnull dereferenceable(1) @.str)
   ret void
 }
 
 ; Function Attrs: nofree nounwind
-declare noundef i32 @printf(ptr noundef readonly captures(none), ...)
+declare  i32 @printf(ptr  readonly captures(none), ...)
 
 ; Function Attrs: nofree norecurse nounwind uwtable
-define dso_local noundef i32 @main() norecurse {
+define dso_local  i32 @main() norecurse {
 ; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-LABEL: define dso_local i32 @main(
 ; CHECK-SAME: ) #[[ATTR0]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    tail call void @bob()
@@ -37,3 +40,6 @@ entry:
   tail call void @bob()
   ret i32 0
 }
+;.
+; CHECK: attributes #[[ATTR0]] = { norecurse }
+;.
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll
index 8264cf33df4eb..e351f60cba2db 100644
--- a/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion.ll
@@ -1,4 +1,4 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
 ; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
 
 ; This test includes a call graph with multiple SCCs. The purpose of this is
@@ -36,9 +36,9 @@ entry:
   ret void
 }
 
-define dso_local noundef i32 @main() norecurse {
+define dso_local  i32 @main() norecurse {
 ; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-LABEL: define dso_local i32 @main(
 ; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    tail call void @f1()
@@ -136,3 +136,6 @@ entry:
   tail call void @fun()
   ret void
 }
+;.
+; CHECK: attributes #[[ATTR0]] = { norecurse }
+;.
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll
index af986eff3e13c..cd940379c5f53 100644
--- a/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_multi_scc_indirect_recursion1.ll
@@ -1,4 +1,4 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
 ; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
 
 ; This test includes a call graph with multiple SCCs. The purpose of this is
@@ -11,9 +11,9 @@
 ; chain. but does not call back f1() and hence f1() can be marked as
 ; norecurse.
 
-define dso_local noundef i32 @main() norecurse {
+define dso_local  i32 @main() norecurse {
 ; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-LABEL: define dso_local i32 @main(
 ; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    tail call void @f1()
@@ -93,3 +93,6 @@ entry:
   tail call void @fun()
   ret void
 }
+;.
+; CHECK: attributes #[[ATTR0]] = { norecurse }
+;.
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll
index 554642ff6963c..461e5dff92905 100644
--- a/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_self_recursive_callee.ll
@@ -1,19 +1,23 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
 ; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
 
 ; This test includes a call graph with a self recursive function.
 ; The purpose of this is to check that norecurse is added to functions
 ; which have a self-recursive function in the call-chain.
 ; The call-chain in this test is as follows
-; main -> bob -> callee1 -> callee2 -> callee3 -> callee4 -> callee5
-; where callee5 is self recursive.
+; main -> bob -> callee1 -> callee2
+; where callee2 is self recursive.
 
 @x = dso_local global i32 4, align 4
 @y = dso_local global i32 2, align 4
 
-define internal void @callee6() {
+;.
+; CHECK: @x = dso_local global i32 4, align 4
+; CHECK: @y = dso_local global i32 2, align 4
+;.
+define internal void @callee2() {
 ; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define internal void @callee6(
+; CHECK-LABEL: define internal void @callee2(
 ; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    [[TMP0:%.*]] = load volatile i32, ptr @y, align 4
@@ -28,17 +32,17 @@ entry:
   ret void
 }
 
-define internal void @callee5(i32 noundef %x) {
-; CHECK-LABEL: define internal void @callee5(
-; CHECK-SAME: i32 noundef [[X:%.*]]) {
+define internal void @callee1(i32  %x) {
+; CHECK-LABEL: define internal void @callee1(
+; CHECK-SAME: i32 [[X:%.*]]) {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[X]], 0
 ; CHECK-NEXT:    br i1 [[CMP]], label %[[IF_THEN:.*]], label %[[IF_END:.*]]
 ; CHECK:       [[IF_THEN]]:
-; CHECK-NEXT:    tail call void @callee5(i32 noundef [[X]])
+; CHECK-NEXT:    tail call void @callee1(i32 [[X]])
 ; CHECK-NEXT:    br label %[[IF_END]]
 ; CHECK:       [[IF_END]]:
-; CHECK-NEXT:    tail call void @callee6()
+; CHECK-NEXT:    tail call void @callee2()
 ; CHECK-NEXT:    ret void
 ;
 entry:
@@ -46,84 +50,30 @@ entry:
   br i1 %cmp, label %if.then, label %if.end
 
 if.then:                                          ; preds = %entry
-  tail call void @callee5(i32 noundef %x)
+  tail call void @callee1(i32  %x)
   br label %if.end
 
 if.end:                                           ; preds = %if.then, %entry
-  tail call void @callee6()
-  ret void
-}
-
-define internal void @callee4() {
-; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define internal void @callee4(
-; CHECK-SAME: ) #[[ATTR0]] {
-; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    [[TMP0:%.*]] = load volatile i32, ptr @x, align 4
-; CHECK-NEXT:    tail call void @callee5(i32 noundef [[TMP0]])
-; CHECK-NEXT:    ret void
-;
-entry:
-  %0 = load volatile i32, ptr @x, align 4
-  tail call void @callee5(i32 noundef %0)
-  ret void
-}
-
-define internal void @callee3() {
-; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define internal void @callee3(
-; CHECK-SAME: ) #[[ATTR0]] {
-; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    tail call void @callee4()
-; CHECK-NEXT:    ret void
-;
-entry:
-  tail call void @callee4()
-  ret void
-}
-
-define internal void @callee2() {
-; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define internal void @callee2(
-; CHECK-SAME: ) #[[ATTR0]] {
-; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    tail call void @callee3()
-; CHECK-NEXT:    ret void
-;
-entry:
-  tail call void @callee3()
-  ret void
-}
-
-define internal void @callee1() {
-; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define internal void @callee1(
-; CHECK-SAME: ) #[[ATTR0]] {
-; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    tail call void @callee2()
-; CHECK-NEXT:    ret void
-;
-entry:
   tail call void @callee2()
   ret void
 }
 
 define internal void @bob() {
-; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define internal void @bob(
-; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-LABEL: define internal void @bob() {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    tail call void @callee1()
+; CHECK-NEXT:    [[TMP0:%.*]] = load volatile i32, ptr @x, align 4
+; CHECK-NEXT:    tail call void @callee2(i32 [[TMP0]])
 ; CHECK-NEXT:    ret void
 ;
 entry:
-  tail call void @callee1()
+  %0 = load volatile i32, ptr @x, align 4
+  tail call void @callee2(i32  %0)
   ret void
 }
 
-define dso_local noundef i32 @main() norecurse {
+define dso_local i32 @main() norecurse {
 ; CHECK: Function Attrs: norecurse
-; CHECK-LABEL: define dso_local noundef i32 @main(
+; CHECK-LABEL: define dso_local i32 @main(
 ; CHECK-SAME: ) #[[ATTR0]] {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    tail call void @bob()
@@ -133,3 +83,6 @@ entry:
   tail call void @bob()
   ret i32 0
 }
+;.
+; CHECK: attributes #[[ATTR0]] = { norecurse }
+;.

>From 69ec3389ac3f98e0c19696fc4d74b522282c700f Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Mon, 29 Sep 2025 15:42:05 +0000
Subject: [PATCH 3/3] Add tests, clarify comments

---
 llvm/lib/Transforms/IPO/FunctionAttrs.cpp     |  6 +-
 .../Transforms/FunctionAttrs/norecurse_lto.ll | 69 +++++++++++++++++++
 .../norecurse_multinode_refscc.ll             | 42 +++++++++++
 3 files changed, 114 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_lto.ll
 create mode 100644 llvm/test/Transforms/FunctionAttrs/norecurse_multinode_refscc.ll

diff --git a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
index ccf2fafb4493b..50130da01c7ba 100644
--- a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
@@ -2472,12 +2472,12 @@ PreservedAnalyses NoRecurseLTOInferencePass::run(Module &M,
 
   for (LazyCallGraph::RefSCC &RC : CG.postorder_ref_sccs()) {
     // Skip any RefSCC that is part of a call cycle. A RefSCC containing more
-    // than one SCC indicates a recursive relationship, which could involve
-    // direct or indirect calls.
+    // than one SCC indicates a recursive relationship involving indirect calls.
     if (RC.size() > 1)
       continue;
 
-    // A single-SCC RefSCC could still be a self-loop.
+    // RefSCC contains a single-SCC. SCC size > 1 indicates mutually recursive
+    // functions. Ex: foo1 -> foo2 -> foo3 -> foo1.
     LazyCallGraph::SCC &S = *RC.begin();
     if (S.size() > 1)
       continue;
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_lto.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_lto.ll
new file mode 100644
index 0000000000000..5be707bef8655
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_lto.ll
@@ -0,0 +1,69 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
+; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
+
+; This test includes a call graph which has a recursive function(foo2) which
+; calls a non-recursive internal function (foo3) satisfying the norecurse
+; attribute criteria.
+
+
+define internal void @foo3() {
+; CHECK: Function Attrs: norecurse
+; CHECK-LABEL: define internal void @foo3(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:    ret void
+;
+  ret void
+}
+
+define internal i32 @foo2(i32 %accum, i32 %n) {
+; CHECK-LABEL: define internal i32 @foo2(
+; CHECK-SAME: i32 [[ACCUM:%.*]], i32 [[N:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[CMP:%.*]] = icmp eq i32 [[N]], 0
+; CHECK-NEXT:    br i1 [[CMP]], label %[[EXIT:.*]], label %[[RECURSE:.*]]
+; CHECK:       [[RECURSE]]:
+; CHECK-NEXT:    [[SUB:%.*]] = sub i32 [[N]], 1
+; CHECK-NEXT:    [[MUL:%.*]] = mul i32 [[ACCUM]], [[SUB]]
+; CHECK-NEXT:    [[CALL:%.*]] = call i32 @foo2(i32 [[MUL]], i32 [[SUB]])
+; CHECK-NEXT:    call void @foo3()
+; CHECK-NEXT:    br label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    [[RES:%.*]] = phi i32 [ [[ACCUM]], %[[ENTRY]] ], [ [[CALL]], %[[RECURSE]] ]
+; CHECK-NEXT:    ret i32 [[RES]]
+;
+entry:
+  %cmp = icmp eq i32 %n, 0
+  br i1 %cmp, label %exit, label %recurse
+
+recurse:
+  %sub = sub i32 %n, 1
+  %mul = mul i32 %accum, %sub
+  %call = call i32 @foo2(i32 %mul, i32 %sub)
+  call void @foo3()
+  br label %exit
+
+exit:
+  %res = phi i32 [ %accum, %entry ], [ %call, %recurse ]
+  ret i32 %res
+}
+
+define internal i32 @foo1() {
+; CHECK-LABEL: define internal i32 @foo1() {
+; CHECK-NEXT:    [[RES:%.*]] = call i32 @foo2(i32 1, i32 5)
+; CHECK-NEXT:    ret i32 [[RES]]
+;
+  %res = call i32 @foo2(i32 1, i32 5)
+  ret i32 %res
+}
+
+define dso_local i32 @main() {
+; CHECK-LABEL: define dso_local i32 @main() {
+; CHECK-NEXT:    [[RES:%.*]] = call i32 @foo1()
+; CHECK-NEXT:    ret i32 [[RES]]
+;
+  %res = call i32 @foo1()
+  ret i32 %res
+}
+;.
+; CHECK: attributes #[[ATTR0]] = { norecurse }
+;.
diff --git a/llvm/test/Transforms/FunctionAttrs/norecurse_multinode_refscc.ll b/llvm/test/Transforms/FunctionAttrs/norecurse_multinode_refscc.ll
new file mode 100644
index 0000000000000..ac5594dde3e22
--- /dev/null
+++ b/llvm/test/Transforms/FunctionAttrs/norecurse_multinode_refscc.ll
@@ -0,0 +1,42 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
+; RUN: opt -passes=norecurse-lto-inference -S %s | FileCheck %s
+
+; This is a negative test which results in RefSCC with size > 1.
+; RefSCC : [(f2), (f1)]
+; --- SCC A (f1) --- size() = 1
+; f1 has its address taken (in main) and calls f2 indirectly.
+define internal void @f1() {
+; CHECK-LABEL: define internal void @f1() {
+; CHECK-NEXT:    call void @f2()
+; CHECK-NEXT:    ret void
+;
+  call void @f2()
+  ret void
+}
+
+; --- SCC B (f2) --- size() = 1
+; f2 indirectly calls f1
+define internal void @f2() {
+; CHECK-LABEL: define internal void @f2() {
+; CHECK-NEXT:    [[FP:%.*]] = alloca ptr, align 8
+; CHECK-NEXT:    store ptr @f1, ptr [[FP]], align 8
+; CHECK-NEXT:    [[TMP:%.*]] = load ptr, ptr [[FP]], align 8
+; CHECK-NEXT:    call void [[TMP]]()
+; CHECK-NEXT:    ret void
+;
+  %fp = alloca void ()*
+  store void ()* @f1, void ()** %fp
+  %tmp = load void ()*, void ()** %fp
+  call void %tmp()
+  ret void
+}
+
+define i32 @main() {
+; CHECK-LABEL: define i32 @main() {
+; CHECK-NEXT:    call void @f1()
+; CHECK-NEXT:    ret i32 0
+;
+  call void @f1()
+  ret i32 0
+}
+



More information about the llvm-commits mailing list