[llvm] [Attr] Add `noipa` function attribute (PR #203304)

J. Ryan Stinnett via llvm-commits llvm-commits at lists.llvm.org
Thu Jun 11 08:57:10 PDT 2026


https://github.com/jryans updated https://github.com/llvm/llvm-project/pull/203304

>From 3063d323973126cbb760e657b236aec5cf28ace8 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <ryan at convolv.es>
Date: Thu, 11 Jun 2026 15:06:51 +0100
Subject: [PATCH 1/3] [Attr] Add `noipa` function attribute

This adds a `noipa` function attribute to LLVM IR. This new attribute
disables any interprocedural analysis that inspects the definition of
the function. Setting this attribute is equivalent to moving the
function definition to a separate, optimizer-opaque, module.

Revival of https://reviews.llvm.org/D101011
Discussed in https://discourse.llvm.org/t/noipa-continues/74411

LLVM portion of https://github.com/llvm/llvm-project/issues/40819
---
 llvm/docs/LangRef.rst                              |  5 +++++
 llvm/include/llvm/Bitcode/LLVMBitCodes.h           |  1 +
 llvm/include/llvm/IR/Attributes.td                 |  3 +++
 llvm/include/llvm/IR/GlobalValue.h                 | 10 +++++++---
 llvm/lib/Bitcode/Reader/BitcodeReader.cpp          |  2 ++
 llvm/lib/Bitcode/Writer/BitcodeWriter.cpp          |  2 ++
 llvm/lib/IR/Globals.cpp                            |  7 +++++++
 llvm/lib/Transforms/IPO/FunctionAttrs.cpp          | 12 ++++++------
 llvm/lib/Transforms/Utils/CodeExtractor.cpp        |  1 +
 llvm/test/Bitcode/attributes.ll                    |  6 ++++++
 llvm/unittests/IR/FunctionTest.cpp                 | 11 +++++++++++
 llvm/utils/emacs/llvm-mode.el                      |  2 +-
 llvm/utils/kate/llvm.xml                           |  1 +
 llvm/utils/vim/syntax/llvm.vim                     |  1 +
 llvm/utils/vscode/llvm/syntaxes/ll.tmLanguage.yaml |  1 +
 15 files changed, 55 insertions(+), 10 deletions(-)

diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index cf052513c5ef8..d2a1a32e2c6df 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -2390,6 +2390,11 @@ For example:
     This attribute indicates that the inliner should never inline this
     function in any situation. This attribute may not be used together
     with the ``alwaysinline`` attribute.
+``noipa``
+    Disables any interprocedural analysis that inspects the definition of this
+    function. Equivalent to moving this function definition to a separate,
+    optimizer-opaque, module. Any attributes on the function are still respected
+    (as they would be if they remained on a function declaration in this module).
 ``nomerge``
     This attribute indicates that calls to this function should never be merged
     during optimization. For example, it will prevent tail merging otherwise
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index 95787c595dff7..5c2b681cc3d63 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -820,6 +820,7 @@ enum AttributeKindCodes {
   ATTR_KIND_DENORMAL_FPENV = 106,
   ATTR_KIND_NOOUTLINE = 107,
   ATTR_KIND_FLATTEN = 108,
+  ATTR_KIND_NO_INTERPROCEDURAL_ANALYSIS = 109,
 };
 
 enum ComdatSelectionKindCodes {
diff --git a/llvm/include/llvm/IR/Attributes.td b/llvm/include/llvm/IR/Attributes.td
index 34032e341d85e..4e45100b54d38 100644
--- a/llvm/include/llvm/IR/Attributes.td
+++ b/llvm/include/llvm/IR/Attributes.td
@@ -218,6 +218,9 @@ def NoInline : EnumAttr<"noinline", IntersectPreserve, [FnAttr]>;
 /// nooutline
 def NoOutline : EnumAttr<"nooutline", IntersectPreserve, [FnAttr]>;
 
+/// Do not do interprocedural analysis or optimization including this function
+def NoIPA : EnumAttr<"noipa", IntersectPreserve, [FnAttr]>;
+
 /// Function is called early and/or often, so lazy binding isn't worthwhile.
 def NonLazyBind : EnumAttr<"nonlazybind", IntersectPreserve, [FnAttr]>;
 
diff --git a/llvm/include/llvm/IR/GlobalValue.h b/llvm/include/llvm/IR/GlobalValue.h
index 83e695cdd27d9..59424f201be84 100644
--- a/llvm/include/llvm/IR/GlobalValue.h
+++ b/llvm/include/llvm/IR/GlobalValue.h
@@ -136,7 +136,7 @@ class GlobalValue : public Constant {
   /// Returns true if the definition of this global may be replaced by a
   /// differently optimized variant of the same source level function at link
   /// time.
-  bool mayBeDerefined() const {
+  bool mayBeDerefinedOrNoIPA() const {
     switch (getLinkage()) {
     case WeakODRLinkage:
     case LinkOnceODRLinkage:
@@ -155,7 +155,7 @@ class GlobalValue : public Constant {
       // nobuiltin due to attributes at call-sites. To avoid applying IPO based
       // on nobuiltin semantics, treat such function definitions as maybe
       // derefined.
-      return isInterposable() || isNobuiltinFnDef();
+      return isInterposable() || isNobuiltinFnDef() || isNoipaFnDef();
     }
 
     llvm_unreachable("Fully covered switch above!");
@@ -165,6 +165,10 @@ class GlobalValue : public Constant {
   /// attribute.
   LLVM_ABI bool isNobuiltinFnDef() const;
 
+  /// Returns true if the global is a function definition with the noipa
+  /// attribute.
+  LLVM_ABI bool isNoipaFnDef() const;
+
 protected:
   /// The intrinsic ID for this subclass (which must be a Function).
   ///
@@ -490,7 +494,7 @@ class GlobalValue : public Constant {
   /// visible variant is *a* correct implementation of the original source
   /// function; it just isn't the *only* correct implementation.
   bool isDefinitionExact() const {
-    return !mayBeDerefined();
+    return !mayBeDerefinedOrNoIPA();
   }
 
   /// Return true if this global has an exact defintion.
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index 87ae36afb4e65..04a62dccea179 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2302,6 +2302,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
     return Attribute::DenormalFPEnv;
   case bitc::ATTR_KIND_NOOUTLINE:
     return Attribute::NoOutline;
+  case bitc::ATTR_KIND_NO_INTERPROCEDURAL_ANALYSIS:
+    return Attribute::NoIPA;
   }
 }
 
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index f4857461ca58e..f8f5c6953f94a 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -1008,6 +1008,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
     return bitc::ATTR_KIND_DENORMAL_FPENV;
   case Attribute::NoOutline:
     return bitc::ATTR_KIND_NOOUTLINE;
+  case Attribute::NoIPA:
+    return bitc::ATTR_KIND_NO_INTERPROCEDURAL_ANALYSIS;
   case Attribute::EndAttrKinds:
     llvm_unreachable("Can not encode end-attribute kinds marker.");
   case Attribute::None:
diff --git a/llvm/lib/IR/Globals.cpp b/llvm/lib/IR/Globals.cpp
index 805595a80a054..422517dd05843 100644
--- a/llvm/lib/IR/Globals.cpp
+++ b/llvm/lib/IR/Globals.cpp
@@ -334,6 +334,13 @@ bool GlobalValue::isNobuiltinFnDef() const {
   return F->hasFnAttribute(Attribute::NoBuiltin);
 }
 
+bool GlobalValue::isNoipaFnDef() const {
+  const Function *F = dyn_cast<Function>(this);
+  if (!F || F->empty())
+    return false;
+  return F->hasFnAttribute(Attribute::NoIPA);
+}
+
 bool GlobalValue::isDeclaration() const {
   // Globals are definitions if they have an initializer.
   if (const GlobalVariable *GV = dyn_cast<GlobalVariable>(this))
diff --git a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
index 467fab519d32a..786ce066132e4 100644
--- a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
@@ -976,7 +976,7 @@ static void addArgumentReturnedAttrs(const SCCNodeSet &SCCNodes,
   for (Function *F : SCCNodes) {
     // We can infer and propagate function attributes only when we know that the
     // definition we'll get at link time is *exactly* the definition we see now.
-    // For more details, see GlobalValue::mayBeDerefined.
+    // For more details, see GlobalValue::mayBeDerefinedOrNoIPA.
     if (!F->hasExactDefinition())
       continue;
 
@@ -1250,7 +1250,7 @@ static void addArgumentAttrs(const SCCNodeSet &SCCNodes,
   for (Function *F : SCCNodes) {
     // We can infer and propagate function attributes only when we know that the
     // definition we'll get at link time is *exactly* the definition we see now.
-    // For more details, see GlobalValue::mayBeDerefined.
+    // For more details, see GlobalValue::mayBeDerefinedOrNoIPA.
     if (!F->hasExactDefinition())
       continue;
 
@@ -1503,7 +1503,7 @@ static void addNoAliasAttrs(const SCCNodeSet &SCCNodes,
 
     // We can infer and propagate function attributes only when we know that the
     // definition we'll get at link time is *exactly* the definition we see now.
-    // For more details, see GlobalValue::mayBeDerefined.
+    // For more details, see GlobalValue::mayBeDerefinedOrNoIPA.
     if (!F->hasExactDefinition())
       return;
 
@@ -1620,7 +1620,7 @@ static void addNonNullAttrs(const SCCNodeSet &SCCNodes,
 
     // We can infer and propagate function attributes only when we know that the
     // definition we'll get at link time is *exactly* the definition we see now.
-    // For more details, see GlobalValue::mayBeDerefined.
+    // For more details, see GlobalValue::mayBeDerefinedOrNoIPA.
     if (!F->hasExactDefinition())
       return;
 
@@ -1674,7 +1674,7 @@ static void addNoUndefAttrs(const SCCNodeSet &SCCNodes,
 
     // We can infer and propagate function attributes only when we know that the
     // definition we'll get at link time is *exactly* the definition we see now.
-    // For more details, see GlobalValue::mayBeDerefined.
+    // For more details, see GlobalValue::mayBeDerefinedOrNoIPA.
     if (!F->hasExactDefinition())
       return;
 
@@ -2154,7 +2154,7 @@ static void addColdAttrs(const SCCNodeSet &SCCNodes,
 static bool functionWillReturn(const Function &F) {
   // We can infer and propagate function attributes only when we know that the
   // definition we'll get at link time is *exactly* the definition we see now.
-  // For more details, see GlobalValue::mayBeDerefined.
+  // For more details, see GlobalValue::mayBeDerefinedOrNoIPA.
   if (!F.hasExactDefinition())
     return false;
 
diff --git a/llvm/lib/Transforms/Utils/CodeExtractor.cpp b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
index 7ffa99878cf74..39631d0c68f07 100644
--- a/llvm/lib/Transforms/Utils/CodeExtractor.cpp
+++ b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
@@ -983,6 +983,7 @@ Function *CodeExtractor::constructFunctionDeclaration(
       case Attribute::NoFree:
       case Attribute::NoImplicitFloat:
       case Attribute::NoInline:
+      case Attribute::NoIPA:
       case Attribute::NoOutline:
       case Attribute::NonLazyBind:
       case Attribute::NoRedZone:
diff --git a/llvm/test/Bitcode/attributes.ll b/llvm/test/Bitcode/attributes.ll
index 21712fae7eecd..f696f2dd12323 100644
--- a/llvm/test/Bitcode/attributes.ll
+++ b/llvm/test/Bitcode/attributes.ll
@@ -592,6 +592,11 @@ define void @dead_on_return_sized(ptr dead_on_return(4) %p) {
   ret void
 }
 
+; CHECK: define void @noipa() [[NOIPA:#[0-9]+]]
+define void @noipa() noipa {
+  ret void
+}
+
 ; CHECK: attributes #0 = { noreturn }
 ; CHECK: attributes #1 = { nounwind }
 ; CHECK: attributes #2 = { memory(none) }
@@ -654,4 +659,5 @@ define void @dead_on_return_sized(ptr dead_on_return(4) %p) {
 ; CHECK: attributes [[SKIPPROFILE]] = { skipprofile }
 ; CHECK: attributes [[OPTDEBUG]] = { optdebug }
 ; CHECK: attributes [[NODIVERGENCESOURCE]] = { nodivergencesource }
+; CHECK: attributes [[NOIPA]] = { noipa }
 ; CHECK: attributes #[[NOBUILTIN]] = { nobuiltin }
diff --git a/llvm/unittests/IR/FunctionTest.cpp b/llvm/unittests/IR/FunctionTest.cpp
index 8ed76999dc13d..c7cd8bd43ecd7 100644
--- a/llvm/unittests/IR/FunctionTest.cpp
+++ b/llvm/unittests/IR/FunctionTest.cpp
@@ -644,4 +644,15 @@ TEST(FunctionTest, LLVMGetOrInsertFunction) {
   EXPECT_EQ(FuncRef, FuncRef2);
 }
 
+TEST(FunctionTest, NoIPAInexact) {
+  LLVMContext Ctx;
+  std::unique_ptr<Module> M = parseIR(Ctx, R"(
+    define void @foo() { bb1: ret void }
+    define void @bar() #0 { bb1: ret void }
+    attributes #0 = { noipa }
+  )");
+  EXPECT_TRUE(M->getFunction("foo")->isDefinitionExact());
+  EXPECT_FALSE(M->getFunction("bar")->isDefinitionExact());
+}
+
 } // end namespace
diff --git a/llvm/utils/emacs/llvm-mode.el b/llvm/utils/emacs/llvm-mode.el
index 7dc1c28489839..6ee5a30e5e54c 100644
--- a/llvm/utils/emacs/llvm-mode.el
+++ b/llvm/utils/emacs/llvm-mode.el
@@ -35,7 +35,7 @@
    `(,(regexp-opt
        '("alwaysinline" "argmemonly" "allocsize" "builtin" "cold" "convergent" "dereferenceable" "dereferenceable_or_null" "hot" "immarg" "inaccessiblememonly"
          "inaccessiblemem_or_argmemonly" "inalloca" "inlinehint" "jumptable" "minsize" "mustprogress" "naked" "nobuiltin" "nonnull" "nocapture"
-         "nocallback" "nocf_check" "noduplicate" "noext" "nofree" "noimplicitfloat" "noinline" "nomerge" "nonlazybind" "noprofile" "noredzone" "noreturn"
+         "nocallback" "nocf_check" "noduplicate" "noext" "nofree" "noimplicitfloat" "noinline" "noipa" "nomerge" "nonlazybind" "noprofile" "noredzone" "noreturn"
          "norecurse" "nosync" "noundef" "nounwind" "nosanitize_bounds" "nosanitize_coverage" "null_pointer_is_valid" "optdebug" "optforfuzzing" "optnone" "optsize" "preallocated" "readnone" "readonly" "returned" "returns_twice"
          "shadowcallstack" "signext" "speculatable" "speculative_load_hardening" "ssp" "sspreq" "sspstrong" "safestack" "sanitize_address" "sanitize_alloc_token" "sanitize_hwaddress" "sanitize_memtag"
          "sanitize_thread" "sanitize_memory" "strictfp" "swifterror" "uwtable" "vscale_range" "willreturn" "writeonly" "zeroext") 'symbols) . font-lock-constant-face)
diff --git a/llvm/utils/kate/llvm.xml b/llvm/utils/kate/llvm.xml
index c66547d9121ce..0291ed32b3f6c 100644
--- a/llvm/utils/kate/llvm.xml
+++ b/llvm/utils/kate/llvm.xml
@@ -104,6 +104,7 @@
       <item> nofree </item>
       <item> noimplicitfloat </item>
       <item> noinline </item>
+      <item> noipa </item>
       <item> nomerge </item>
       <item> nooutline </item>
       <item> noprofile </item>
diff --git a/llvm/utils/vim/syntax/llvm.vim b/llvm/utils/vim/syntax/llvm.vim
index d921e8bd1e401..bb805dff10a7b 100644
--- a/llvm/utils/vim/syntax/llvm.vim
+++ b/llvm/utils/vim/syntax/llvm.vim
@@ -132,6 +132,7 @@ syn keyword llvmKeyword
       \ nofree
       \ noimplicitfloat
       \ noinline
+      \ noipa
       \ nomerge
       \ nooutline
       \ nonlazybind
diff --git a/llvm/utils/vscode/llvm/syntaxes/ll.tmLanguage.yaml b/llvm/utils/vscode/llvm/syntaxes/ll.tmLanguage.yaml
index 06d2e43bb3c2e..6a6caccb350a5 100644
--- a/llvm/utils/vscode/llvm/syntaxes/ll.tmLanguage.yaml
+++ b/llvm/utils/vscode/llvm/syntaxes/ll.tmLanguage.yaml
@@ -229,6 +229,7 @@ patterns:
             \\bnofree\\b|\
             \\bnoimplicitfloat\\b|\
             \\bnoinline\\b|\
+            \\bnoipa\\b|\
             \\bnomerge\\b|\
             \\bnooutline\\b|\
             \\bnonlazybind\\b|\

>From 5da5ac739caabbf554b633c737c7f86c8c718973 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <ryan at convolv.es>
Date: Thu, 11 Jun 2026 16:30:15 +0100
Subject: [PATCH 2/3] Tweak formatting

---
 llvm/include/llvm/IR/GlobalValue.h | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/llvm/include/llvm/IR/GlobalValue.h b/llvm/include/llvm/IR/GlobalValue.h
index 59424f201be84..6e46eced45873 100644
--- a/llvm/include/llvm/IR/GlobalValue.h
+++ b/llvm/include/llvm/IR/GlobalValue.h
@@ -493,9 +493,7 @@ class GlobalValue : public Constant {
   /// interposable (see \c isInterposable), since in such cases the currently
   /// visible variant is *a* correct implementation of the original source
   /// function; it just isn't the *only* correct implementation.
-  bool isDefinitionExact() const {
-    return !mayBeDerefinedOrNoIPA();
-  }
+  bool isDefinitionExact() const { return !mayBeDerefinedOrNoIPA(); }
 
   /// Return true if this global has an exact defintion.
   bool hasExactDefinition() const {

>From 78a881c945f3d225df2a27db2cd9d45ab6526850 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <ryan at convolv.es>
Date: Thu, 11 Jun 2026 16:56:48 +0100
Subject: [PATCH 3/3] Clarify interaction with inlining

---
 llvm/docs/LangRef.rst | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index d2a1a32e2c6df..8d6f436161a3b 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -2392,9 +2392,12 @@ For example:
     with the ``alwaysinline`` attribute.
 ``noipa``
     Disables any interprocedural analysis that inspects the definition of this
-    function. Equivalent to moving this function definition to a separate,
-    optimizer-opaque, module. Any attributes on the function are still respected
-    (as they would be if they remained on a function declaration in this module).
+    function. This attribute is equivalent to moving this function definition to
+    a separate, optimizer-opaque, module. Any attributes on the function are
+    still respected (as they would be if they remained on a function declaration
+    in this module). This attribute does *not* control inlining, which is
+    treated as a separate dimension. Add the ``noinline`` attribute as well in
+    cases where inlining should additionally be disabled.
 ``nomerge``
     This attribute indicates that calls to this function should never be merged
     during optimization. For example, it will prevent tail merging otherwise



More information about the llvm-commits mailing list