[llvm] [IR] Define empty and malformed !callees semantics (PR #221550)

via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 7 23:10:13 PDT 2026


https://github.com/mmiftahx updated https://github.com/llvm/llvm-project/pull/221550

>From c74e806951b70800302aa4ba1faaaab595713e88 Mon Sep 17 00:00:00 2001
From: mmiftahx <mmiftah.duna at gmail.com>
Date: Sun, 6 Sep 2026 04:38:37 -0500
Subject: [PATCH 1/3] [IR] Define empty and malformed !callees semantics

Define !callees as an exhaustive target list whose operand order and
duplication are semantically irrelevant. An empty node is an exhaustive empty
set, while absence provides no target information. Ignore the complete
attachment if it is placed on a non-indirect call or contains a non-Function
operand.

Add CallBase::getCalleesMetadata as a shared fail-closed decoder and migrate
module summary analysis, Attributor, and AMDGPU module splitting to it. Preserve
frontend-provided targets in Attributor when pointer flow leaves and later
re-enters the IR module.

Add IR, Attributor, ThinLTO, and AMDGPU splitting coverage for the new contract.
---
 llvm/docs/LangRef.md                          |  21 ++-
 llvm/include/llvm/IR/InstrTypes.h             |   9 ++
 llvm/include/llvm/IR/MDBuilder.h              |   4 +-
 llvm/lib/Analysis/ModuleSummaryAnalysis.cpp   |  17 +--
 llvm/lib/IR/Instructions.cpp                  |  23 +++
 llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp  |  28 +---
 .../Transforms/IPO/AttributorAttributes.cpp   |  22 ++-
 .../X86/Inputs/callees-metadata-malformed.ll  |  13 ++
 .../ThinLTO/X86/callees-metadata-malformed.ll |  23 +++
 .../callees-metadata-external-roundtrip.ll    |  25 ++++
 .../Attributor/callees-metadata-malformed.ll  |  33 +++++
 .../kernels-dependency-indirect-callee-md.ll  |  11 ++
 llvm/unittests/IR/InstructionsTest.cpp        | 134 ++++++++++++++++++
 13 files changed, 314 insertions(+), 49 deletions(-)
 create mode 100644 llvm/test/ThinLTO/X86/Inputs/callees-metadata-malformed.ll
 create mode 100644 llvm/test/ThinLTO/X86/callees-metadata-malformed.ll
 create mode 100644 llvm/test/Transforms/Attributor/callees-metadata-external-roundtrip.ll
 create mode 100644 llvm/test/Transforms/Attributor/callees-metadata-malformed.ll

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 14caff88243c1..0cfc15a681e56 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7778,12 +7778,21 @@ Example (assuming 64-bit pointers):
 
 #### '`callees`' Metadata
 
-`callees` metadata may be attached to indirect call sites. If `callees`
-metadata is attached to a call site, and any callee is not among the set of
-functions provided by the metadata, the behavior is undefined. The intent of
-this metadata is to facilitate optimizations such as indirect-call promotion.
-For example, in the code below, the call instruction may only target the
-`add` or `sub` functions:
+`callees` metadata may be attached to indirect call sites.
+Its operands provide an exhaustive list of possible callees.
+The list may be conservative: a listed function need not be dynamically feasible, but on every defined execution of the call the callee must be one of the listed functions.
+The order and duplication of operands are semantically irrelevant.
+The intent of this metadata is to facilitate optimizations such as indirect-call promotion.
+
+An empty node is an exhaustive empty set, so executing the call has undefined behavior.
+If the metadata is absent, no exhaustive callee information is provided.
+
+Each operand must refer to a `Function`.
+If any operand does not, the entire attachment provides no information and must be ignored.
+This includes null operands left behind when a function referenced only through metadata is deleted.
+An attachment on a non-indirect call likewise provides no information.
+
+For example, in the code below, the call instruction may only target the `add` or `sub` functions:
 
 ```llvm
 %result = call i64 %binop(i64 %x, i64 %y), !callees !0
diff --git a/llvm/include/llvm/IR/InstrTypes.h b/llvm/include/llvm/IR/InstrTypes.h
index 5f7df6a4eb6f8..b5a0932069a4c 100644
--- a/llvm/include/llvm/IR/InstrTypes.h
+++ b/llvm/include/llvm/IR/InstrTypes.h
@@ -1424,6 +1424,15 @@ class CallBase : public Instruction {
   /// Return true if the callsite is an indirect call.
   LLVM_ABI bool isIndirectCall() const;
 
+  /// Decode the exhaustive list of possible callees from !callees metadata.
+  ///
+  /// Return true for a well-formed attachment on an indirect call, including
+  /// an empty attachment. Return false if the attachment is absent, is on a
+  /// non-indirect call, or contains an operand that is not a Function.
+  /// \p Callees is cleared on failure. On success, duplicate functions are
+  /// omitted while preserving the order of their first occurrence.
+  LLVM_ABI bool getCalleesMetadata(SmallVectorImpl<Function *> &Callees) const;
+
   /// Determine whether the passed iterator points to the callee operand's Use.
   bool isCallee(Value::const_user_iterator UI) const {
     return isCallee(&UI.getUse());
diff --git a/llvm/include/llvm/IR/MDBuilder.h b/llvm/include/llvm/IR/MDBuilder.h
index e88de73567c35..1db8007e40598 100644
--- a/llvm/include/llvm/IR/MDBuilder.h
+++ b/llvm/include/llvm/IR/MDBuilder.h
@@ -131,8 +131,8 @@ class MDBuilder {
   // Callees metadata.
   //===------------------------------------------------------------------===//
 
-  /// Return metadata indicating the possible callees of indirect
-  /// calls.
+  /// Return metadata indicating the exhaustive list of possible callees of
+  /// indirect calls.
   LLVM_ABI MDNode *createCallees(ArrayRef<Function *> Callees);
 
   //===------------------------------------------------------------------===//
diff --git a/llvm/lib/Analysis/ModuleSummaryAnalysis.cpp b/llvm/lib/Analysis/ModuleSummaryAnalysis.cpp
index bb5fd4eefb7f9..0c69032fc3adc 100644
--- a/llvm/lib/Analysis/ModuleSummaryAnalysis.cpp
+++ b/llvm/lib/Analysis/ModuleSummaryAnalysis.cpp
@@ -512,17 +512,14 @@ static void computeFunctionSummary(
         if (!CalledValue || isa<Constant>(CalledValue))
           continue;
 
-        // Check if the instruction has a callees metadata. If so, add callees
-        // to CallGraphEdges to reflect the references from the metadata, and
-        // to enable importing for subsequent indirect call promotion and
+        // If the instruction has valid callees metadata, add its callees to
+        // CallGraphEdges to reflect the references from the metadata, and to
+        // enable importing for subsequent indirect call promotion and
         // inlining.
-        if (auto *MD = I.getMetadata(LLVMContext::MD_callees)) {
-          for (const auto &Op : MD->operands()) {
-            Function *Callee = mdconst::extract_or_null<Function>(Op);
-            if (Callee)
-              CallGraphEdges[Index.getOrInsertValueInfo(Callee)];
-          }
-        }
+        SmallVector<Function *, 4> Callees;
+        if (CB->getCalleesMetadata(Callees))
+          for (Function *Callee : Callees)
+            CallGraphEdges[Index.getOrInsertValueInfo(Callee)];
 
         CandidateProfileData =
             ICallAnalysis.getPromotionCandidatesForInstruction(
diff --git a/llvm/lib/IR/Instructions.cpp b/llvm/lib/IR/Instructions.cpp
index 325850b0a880d..b535eb7a4a680 100644
--- a/llvm/lib/IR/Instructions.cpp
+++ b/llvm/lib/IR/Instructions.cpp
@@ -14,6 +14,7 @@
 #include "llvm/IR/Instructions.h"
 #include "LLVMContextImpl.h"
 #include "llvm/ADT/SmallBitVector.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/Twine.h"
 #include "llvm/IR/Attributes.h"
@@ -341,6 +342,28 @@ bool CallBase::isIndirectCall() const {
   return !isInlineAsm();
 }
 
+bool CallBase::getCalleesMetadata(SmallVectorImpl<Function *> &Callees) const {
+  Callees.clear();
+  if (!isIndirectCall())
+    return false;
+
+  const MDNode *MD = getMetadata(LLVMContext::MD_callees);
+  if (!MD)
+    return false;
+
+  SmallPtrSet<Function *, 4> Seen;
+  for (const MDOperand &Op : MD->operands()) {
+    Function *Callee = mdconst::dyn_extract_or_null<Function>(Op);
+    if (!Callee) {
+      Callees.clear();
+      return false;
+    }
+    if (Seen.insert(Callee).second)
+      Callees.push_back(Callee);
+  }
+  return true;
+}
+
 /// Tests if this call site must be tail call optimized. Only a CallInst can
 /// be tail call optimized.
 bool CallBase::isMustTailCall() const {
diff --git a/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp b/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
index 05857d88a597c..ba5929736d5d4 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
@@ -475,29 +475,6 @@ void SplitGraph::Node::visitAllDependencies(
   }
 }
 
-/// Checks if \p I has MD_callees and if it does, parse it and put the function
-/// in \p Callees.
-///
-/// \returns true if there was metadata and it was parsed correctly. false if
-/// there was no MD or if it contained unknown entries and parsing failed.
-/// If this returns false, \p Callees will contain incomplete information
-/// and must not be used.
-static bool handleCalleesMD(const Instruction &I,
-                            SetVector<Function *> &Callees) {
-  auto *MD = I.getMetadata(LLVMContext::MD_callees);
-  if (!MD)
-    return false;
-
-  for (const auto &Op : MD->operands()) {
-    Function *Callee = mdconst::extract_or_null<Function>(Op);
-    if (!Callee)
-      return false;
-    Callees.insert(Callee);
-  }
-
-  return true;
-}
-
 void SplitGraph::buildGraph(CallGraph &CG) {
   SplitModuleTimer SMT("buildGraph", "graph construction");
   LLVM_DEBUG(
@@ -550,8 +527,11 @@ void SplitGraph::buildGraph(CallGraph &CG) {
           continue;
         }
 
-        if (handleCalleesMD(Inst, KnownCallees))
+        SmallVector<Function *, 4> CallCallees;
+        if (CB->getCalleesMetadata(CallCallees)) {
+          KnownCallees.insert_range(CallCallees);
           continue;
+        }
         // If we failed to parse any !callees MD, or some was missing,
         // the entire KnownCallees list is now unreliable.
         KnownCallees.clear();
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 8d16f49525ca8..e9df83bbc8c5d 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -12372,14 +12372,13 @@ struct AAIndirectCallInfoCallSite : public AAIndirectCallInfo {
 
   /// See AbstractAttribute::initialize(...).
   void initialize(Attributor &A) override {
-    auto *MD = getCtxI()->getMetadata(LLVMContext::MD_callees);
-    if (!MD && !A.isClosedWorldModule())
+    SmallVector<Function *, 4> Callees;
+    HasCalleesMetadata = cast<CallBase>(getCtxI())->getCalleesMetadata(Callees);
+    if (!HasCalleesMetadata && !A.isClosedWorldModule())
       return;
 
-    if (MD) {
-      for (const auto &Op : MD->operands())
-        if (Function *Callee = mdconst::dyn_extract_or_null<Function>(Op))
-          PotentialCallees.insert(Callee);
+    if (HasCalleesMetadata) {
+      PotentialCallees.insert_range(Callees);
     } else if (A.isClosedWorldModule()) {
       ArrayRef<Function *> IndirectlyCallableFunctions =
           A.getInfoCache().getIndirectlyCallableFunctions(A);
@@ -12400,6 +12399,11 @@ struct AAIndirectCallInfoCallSite : public AAIndirectCallInfo {
 
     auto CheckPotentialCalleeUse = [&](Function &PotentialCallee,
                                        bool &UsedAssumedInformation) {
+      // A semantic !callees list can describe pointer flow through code
+      // outside this IR module. Do not remove a listed target merely because
+      // AAGlobalValueInfo cannot find an in-module path to the call operand.
+      if (HasCalleesMetadata)
+        return true;
       const auto *GIAA = A.getAAFor<AAGlobalValueInfo>(
           *this, IRPosition::value(PotentialCallee), DepClassTy::OPTIONAL);
       if (!GIAA || GIAA->isPotentialUse(CalleeUse))
@@ -12674,10 +12678,14 @@ struct AAIndirectCallInfoCallSite : public AAIndirectCallInfo {
   /// Map to remember filter results.
   DenseMap<Function *, std::optional<bool>> FilterResults;
 
-  /// If the !callee metadata was present, this set will contain all potential
+  /// If !callees metadata was present, this set will contain all potential
   /// callees (superset).
   SmallSetVector<Function *, 4> PotentialCallees;
 
+  /// Whether PotentialCallees came from semantic !callees metadata rather than
+  /// the Attributor's closed-world candidate inventory.
+  bool HasCalleesMetadata = false;
+
   /// This set contains all currently assumed calllees, which might grow over
   /// time.
   SmallSetVector<Function *, 4> AssumedCallees;
diff --git a/llvm/test/ThinLTO/X86/Inputs/callees-metadata-malformed.ll b/llvm/test/ThinLTO/X86/Inputs/callees-metadata-malformed.ll
new file mode 100644
index 0000000000000..568cdb021e0f9
--- /dev/null
+++ b/llvm/test/ThinLTO/X86/Inputs/callees-metadata-malformed.ll
@@ -0,0 +1,13 @@
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+define void @callee(ptr %target) {
+  call void %target(), !callees !0
+  ret void
+}
+
+define internal void @metadata_only_target() {
+  ret void
+}
+
+!0 = !{ptr @metadata_only_target, ptr null}
diff --git a/llvm/test/ThinLTO/X86/callees-metadata-malformed.ll b/llvm/test/ThinLTO/X86/callees-metadata-malformed.ll
new file mode 100644
index 0000000000000..beed8e19d79e6
--- /dev/null
+++ b/llvm/test/ThinLTO/X86/callees-metadata-malformed.ll
@@ -0,0 +1,23 @@
+; RUN: opt -module-summary %s -o %t1.bc
+; RUN: opt -module-summary %p/Inputs/callees-metadata-malformed.ll -o %t2.bc
+; RUN: llvm-lto2 run %t1.bc %t2.bc -o %t.o -save-temps \
+; RUN:     -r=%t1.bc,caller,plx \
+; RUN:     -r=%t1.bc,callee,l \
+; RUN:     -r=%t2.bc,callee,pl
+; RUN: llvm-dis %t.o.1.3.import.bc -o - | FileCheck %s
+
+; A mixed malformed !callees node must not add an edge for its valid-looking
+; operand to the module summary.
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; CHECK: define available_externally void @callee
+; CHECK-NOT: define {{.*}} @metadata_only_target
+
+define void @caller(ptr %target) {
+  call void @callee(ptr %target)
+  ret void
+}
+
+declare void @callee(ptr)
diff --git a/llvm/test/Transforms/Attributor/callees-metadata-external-roundtrip.ll b/llvm/test/Transforms/Attributor/callees-metadata-external-roundtrip.ll
new file mode 100644
index 0000000000000..93630b6cb7b26
--- /dev/null
+++ b/llvm/test/Transforms/Attributor/callees-metadata-external-roundtrip.ll
@@ -0,0 +1,25 @@
+; RUN: opt -passes=attributor -S %s | FileCheck %s
+
+target triple = "amdgcn-amd-amdhsa"
+
+; The host may read @h and later pass or store that pointer for @k. Semantic
+; !callees is the authority for this external round trip; the absence of an
+; in-module use path from @f to %p cannot erase the call and its volatile side
+; effect.
+ at h = protected constant ptr @f
+ at sink = protected global i32 0
+ at llvm.used = appending global [1 x ptr] [ptr @h], section "llvm.metadata"
+
+define hidden void @f() noinline {
+  store volatile i32 1, ptr @sink
+  ret void
+}
+
+define amdgpu_kernel void @k(ptr %p) {
+; CHECK-LABEL: define amdgpu_kernel void @k(
+; CHECK: call void @f()
+  call void %p(), !callees !0
+  ret void
+}
+
+!0 = !{ptr @f}
diff --git a/llvm/test/Transforms/Attributor/callees-metadata-malformed.ll b/llvm/test/Transforms/Attributor/callees-metadata-malformed.ll
new file mode 100644
index 0000000000000..8783a842abdd5
--- /dev/null
+++ b/llvm/test/Transforms/Attributor/callees-metadata-malformed.ll
@@ -0,0 +1,33 @@
+; RUN: opt -passes='globaldce,attributor' -S %s | FileCheck %s
+
+; A function referenced only by metadata can be deleted, leaving a raw null
+; operand behind. The resulting malformed !callees attachment must be ignored
+; rather than interpreted as an exhaustive empty set.
+
+define void @caller(ptr %callee) {
+; CHECK-LABEL: define void @caller(
+; CHECK:         call void %callee(), !callees ![[CALLEES:[0-9]+]]
+; CHECK-NEXT:    ret void
+  call void %callee(), !callees !0
+  ret void
+}
+
+define internal void @metadata_only_target() {
+  ret void
+}
+
+; CHECK-NOT: define internal void @metadata_only_target
+
+; A well-formed empty attachment is an exhaustive empty set. Reaching the call
+; is therefore undefined, and Attributor may make the call site unreachable.
+define void @empty_callees(ptr %callee) {
+; CHECK-LABEL: define void @empty_callees(
+; CHECK-NEXT:    unreachable
+  call void %callee(), !callees !1
+  ret void
+}
+
+; CHECK: ![[CALLEES]] = distinct !{null}
+
+!0 = !{ptr @metadata_only_target}
+!1 = !{}
diff --git a/llvm/test/tools/llvm-split/AMDGPU/kernels-dependency-indirect-callee-md.ll b/llvm/test/tools/llvm-split/AMDGPU/kernels-dependency-indirect-callee-md.ll
index e1a8e5fe40a07..e170b7cb1d37f 100644
--- a/llvm/test/tools/llvm-split/AMDGPU/kernels-dependency-indirect-callee-md.ll
+++ b/llvm/test/tools/llvm-split/AMDGPU/kernels-dependency-indirect-callee-md.ll
@@ -8,6 +8,17 @@
 ; RUN: llvm-dis -o - %t-nomd1 | FileCheck --check-prefix=CHECK-NOMD1 --implicit-check-not=define %s
 ; RUN: llvm-dis -o - %t-nomd2 | FileCheck --check-prefix=CHECK-NOMD2 --implicit-check-not=define %s
 
+; A malformed attachment must behave like an absent attachment. In particular,
+; do not retain the valid-looking prefix of a mixed node.
+; RUN: sed 's/_MD_/, !callees !{ptr @CallCandidate0, ptr null}/g' %s | \
+; RUN:   llvm-split -o %t-malformed -j 3 -mtriple amdgpu-amd-amdhsa
+; RUN: llvm-dis -o - %t-malformed0 | \
+; RUN:   FileCheck --check-prefix=CHECK-NOMD0 --implicit-check-not=define %s
+; RUN: llvm-dis -o - %t-malformed1 | \
+; RUN:   FileCheck --check-prefix=CHECK-NOMD1 --implicit-check-not=define %s
+; RUN: llvm-dis -o - %t-malformed2 | \
+; RUN:   FileCheck --check-prefix=CHECK-NOMD2 --implicit-check-not=define %s
+
 ; CHECK0: define internal void @HelperC
 ; CHECK0: define amdgpu_kernel void @C
 
diff --git a/llvm/unittests/IR/InstructionsTest.cpp b/llvm/unittests/IR/InstructionsTest.cpp
index 5f92f325f793f..900dbb00e26e2 100644
--- a/llvm/unittests/IR/InstructionsTest.cpp
+++ b/llvm/unittests/IR/InstructionsTest.cpp
@@ -20,7 +20,11 @@
 #include "llvm/IR/DerivedTypes.h"
 #include "llvm/IR/FPEnv.h"
 #include "llvm/IR/Function.h"
+#include "llvm/IR/GlobalAlias.h"
+#include "llvm/IR/GlobalIFunc.h"
+#include "llvm/IR/GlobalVariable.h"
 #include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InlineAsm.h"
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/MDBuilder.h"
 #include "llvm/IR/Module.h"
@@ -130,6 +134,136 @@ TEST_F(ModuleWithFunctionTest, InvokeInst) {
   }
 }
 
+TEST(InstructionsTest, CalleesMetadataDecoding) {
+  LLVMContext C;
+  Module M("test", C);
+  FunctionType *CalleeTy = FunctionType::get(Type::getVoidTy(C), false);
+  Function *Target0 =
+      Function::Create(CalleeTy, GlobalValue::ExternalLinkage, "target0", M);
+  Function *Target1 =
+      Function::Create(CalleeTy, GlobalValue::ExternalLinkage, "target1", M);
+
+  FunctionType *CallerTy =
+      FunctionType::get(Type::getVoidTy(C), PointerType::getUnqual(C), false);
+  Function *Caller =
+      Function::Create(CallerTy, GlobalValue::ExternalLinkage, "caller", M);
+  BasicBlock *Entry = BasicBlock::Create(C, "entry", Caller);
+  CallInst *Indirect =
+      CallInst::Create(CalleeTy, Caller->getArg(0), {}, "", Entry);
+  CallInst *Direct = CallInst::Create(Target0, {}, "", Entry);
+  InlineAsm *Asm = InlineAsm::get(CalleeTy, "", "", true);
+  CallInst *InlineAsmCall = CallInst::Create(CalleeTy, Asm, {}, "", Entry);
+  ReturnInst::Create(C, Entry);
+
+  SmallVector<Function *, 4> Callees = {Target0};
+  EXPECT_FALSE(Indirect->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+
+  Indirect->setMetadata(LLVMContext::MD_callees, MDNode::get(C, {}));
+  EXPECT_TRUE(Indirect->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+
+  MDBuilder MDB(C);
+  Indirect->setMetadata(LLVMContext::MD_callees,
+                        MDB.createCallees({Target0, Target1, Target0}));
+  ASSERT_TRUE(Indirect->getCalleesMetadata(Callees));
+  ASSERT_EQ(Callees.size(), 2u);
+  EXPECT_EQ(Callees[0], Target0);
+  EXPECT_EQ(Callees[1], Target1);
+
+  MDNode *Valid = MDB.createCallees({Target0});
+  Direct->setMetadata(LLVMContext::MD_callees, Valid);
+  Callees.push_back(Target1);
+  EXPECT_FALSE(Direct->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+
+  InlineAsmCall->setMetadata(LLVMContext::MD_callees, Valid);
+  Callees.push_back(Target1);
+  EXPECT_FALSE(InlineAsmCall->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+
+  Function *Invoker =
+      Function::Create(CallerTy, GlobalValue::ExternalLinkage, "invoker", M);
+  BasicBlock *InvokeEntry = BasicBlock::Create(C, "entry", Invoker);
+  BasicBlock *Normal = BasicBlock::Create(C, "normal", Invoker);
+  BasicBlock *Unwind = BasicBlock::Create(C, "unwind", Invoker);
+  InvokeInst *Invoke = InvokeInst::Create(CalleeTy, Invoker->getArg(0), Normal,
+                                          Unwind, {}, "", InvokeEntry);
+  ReturnInst::Create(C, Normal);
+  new UnreachableInst(C, Unwind);
+  Invoke->setMetadata(LLVMContext::MD_callees,
+                      MDB.createCallees({Target1, Target0}));
+  ASSERT_TRUE(Invoke->getCalleesMetadata(Callees));
+  ASSERT_EQ(Callees.size(), 2u);
+  EXPECT_EQ(Callees[0], Target1);
+  EXPECT_EQ(Callees[1], Target0);
+}
+
+TEST(InstructionsTest, CalleesMetadataRejectsMalformedOperands) {
+  LLVMContext C;
+  Module M("test", C);
+  FunctionType *CalleeTy = FunctionType::get(Type::getVoidTy(C), false);
+  Function *Target =
+      Function::Create(CalleeTy, GlobalValue::ExternalLinkage, "target", M);
+  FunctionType *CallerTy =
+      FunctionType::get(Type::getVoidTy(C), PointerType::getUnqual(C), false);
+  Function *Caller =
+      Function::Create(CallerTy, GlobalValue::ExternalLinkage, "caller", M);
+  BasicBlock *Entry = BasicBlock::Create(C, "entry", Caller);
+  CallInst *Indirect =
+      CallInst::Create(CalleeTy, Caller->getArg(0), {}, "", Entry);
+  ReturnInst::Create(C, Entry);
+
+  auto *Global =
+      new GlobalVariable(M, Type::getInt8Ty(C), false,
+                         GlobalValue::ExternalLinkage, nullptr, "global");
+  GlobalAlias *Alias = GlobalAlias::create(
+      Target->getType(), 0, GlobalValue::ExternalLinkage, "alias", Target, &M);
+  GlobalIFunc *IFunc = GlobalIFunc::create(
+      Target->getType(), 0, GlobalValue::ExternalLinkage, "ifunc", Target, &M);
+
+  SmallVector<Function *, 4> Callees;
+  auto CheckMalformed = [&](StringRef Name, Metadata *BadOperand) {
+    SCOPED_TRACE(Name);
+    Metadata *Operands[] = {ConstantAsMetadata::get(Target), BadOperand};
+    Indirect->setMetadata(LLVMContext::MD_callees, MDNode::get(C, Operands));
+    Callees.push_back(Target);
+    EXPECT_FALSE(Indirect->getCalleesMetadata(Callees));
+    EXPECT_TRUE(Callees.empty());
+  };
+
+  CheckMalformed("raw null", nullptr);
+  CheckMalformed("typed null", ConstantAsMetadata::get(ConstantPointerNull::get(
+                                   PointerType::getUnqual(C))));
+  CheckMalformed("undef", ConstantAsMetadata::get(
+                              UndefValue::get(PointerType::getUnqual(C))));
+  CheckMalformed("poison", ConstantAsMetadata::get(
+                               PoisonValue::get(PointerType::getUnqual(C))));
+  CheckMalformed("global", ConstantAsMetadata::get(Global));
+  CheckMalformed("alias", ConstantAsMetadata::get(Alias));
+  CheckMalformed("ifunc", ConstantAsMetadata::get(IFunc));
+  CheckMalformed("constant expression",
+                 ConstantAsMetadata::get(
+                     ConstantExpr::getPtrToInt(Target, Type::getInt64Ty(C))));
+  CheckMalformed("integer", ConstantAsMetadata::get(
+                                ConstantInt::get(Type::getInt32Ty(C), 0)));
+  CheckMalformed("string", MDString::get(C, "not a function"));
+  CheckMalformed("nested node", MDNode::get(C, {}));
+  CheckMalformed("local value", ValueAsMetadata::get(Caller->getArg(0)));
+
+  Function *DeletedTarget =
+      Function::Create(CalleeTy, GlobalValue::ExternalLinkage, "deleted", M);
+  Indirect->setMetadata(
+      LLVMContext::MD_callees,
+      MDNode::getDistinct(C, ConstantAsMetadata::get(DeletedTarget)));
+  DeletedTarget->eraseFromParent();
+  ASSERT_EQ(Indirect->getMetadata(LLVMContext::MD_callees)->getOperand(0).get(),
+            nullptr);
+  Callees.push_back(Target);
+  EXPECT_FALSE(Indirect->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+}
+
 TEST(InstructionsTest, UncondBrInst) {
   LLVMContext C;
 

>From 3af4c62f7966773064afaa6449c36bcf538a3b99 Mon Sep 17 00:00:00 2001
From: mmiftahx <mmiftah.duna at gmail.com>
Date: Sun, 6 Sep 2026 07:29:41 -0500
Subject: [PATCH 2/3] fixup! [IR] Define empty and malformed !callees semantics

---
 llvm/docs/LangRef.md                   |  7 ++-
 llvm/include/llvm/IR/InstrTypes.h      |  7 +--
 llvm/include/llvm/IR/MDBuilder.h       |  2 +-
 llvm/lib/IR/Instructions.cpp           |  3 --
 llvm/unittests/IR/InstructionsTest.cpp | 64 ++++++++++++++++++++++++--
 5 files changed, 70 insertions(+), 13 deletions(-)

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 0cfc15a681e56..6145e5197fbd1 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7778,19 +7778,22 @@ Example (assuming 64-bit pointers):
 
 #### '`callees`' Metadata
 
-`callees` metadata may be attached to indirect call sites.
+`callees` metadata may be attached to call sites.
 Its operands provide an exhaustive list of possible callees.
 The list may be conservative: a listed function need not be dynamically feasible, but on every defined execution of the call the callee must be one of the listed functions.
 The order and duplication of operands are semantically irrelevant.
 The intent of this metadata is to facilitate optimizations such as indirect-call promotion.
 
+The constraint applies whether the called operand is a constant or not.
+Executing a direct call whose target is not in the list has undefined behavior.
+If the direct target is in the list, the attachment is redundant and may be dropped.
+
 An empty node is an exhaustive empty set, so executing the call has undefined behavior.
 If the metadata is absent, no exhaustive callee information is provided.
 
 Each operand must refer to a `Function`.
 If any operand does not, the entire attachment provides no information and must be ignored.
 This includes null operands left behind when a function referenced only through metadata is deleted.
-An attachment on a non-indirect call likewise provides no information.
 
 For example, in the code below, the call instruction may only target the `add` or `sub` functions:
 
diff --git a/llvm/include/llvm/IR/InstrTypes.h b/llvm/include/llvm/IR/InstrTypes.h
index b5a0932069a4c..c83ea3a84a505 100644
--- a/llvm/include/llvm/IR/InstrTypes.h
+++ b/llvm/include/llvm/IR/InstrTypes.h
@@ -1426,9 +1426,10 @@ class CallBase : public Instruction {
 
   /// Decode the exhaustive list of possible callees from !callees metadata.
   ///
-  /// Return true for a well-formed attachment on an indirect call, including
-  /// an empty attachment. Return false if the attachment is absent, is on a
-  /// non-indirect call, or contains an operand that is not a Function.
+  /// Return true for a well-formed attachment, including an empty attachment.
+  /// Return false if the attachment is absent or contains an operand that is
+  /// not a Function. The result does not depend on the called operand and does
+  /// not check whether that operand satisfies the callee constraint.
   /// \p Callees is cleared on failure. On success, duplicate functions are
   /// omitted while preserving the order of their first occurrence.
   LLVM_ABI bool getCalleesMetadata(SmallVectorImpl<Function *> &Callees) const;
diff --git a/llvm/include/llvm/IR/MDBuilder.h b/llvm/include/llvm/IR/MDBuilder.h
index 1db8007e40598..1a3a21405276f 100644
--- a/llvm/include/llvm/IR/MDBuilder.h
+++ b/llvm/include/llvm/IR/MDBuilder.h
@@ -132,7 +132,7 @@ class MDBuilder {
   //===------------------------------------------------------------------===//
 
   /// Return metadata indicating the exhaustive list of possible callees of
-  /// indirect calls.
+  /// calls.
   LLVM_ABI MDNode *createCallees(ArrayRef<Function *> Callees);
 
   //===------------------------------------------------------------------===//
diff --git a/llvm/lib/IR/Instructions.cpp b/llvm/lib/IR/Instructions.cpp
index b535eb7a4a680..afd1c917eb66c 100644
--- a/llvm/lib/IR/Instructions.cpp
+++ b/llvm/lib/IR/Instructions.cpp
@@ -344,9 +344,6 @@ bool CallBase::isIndirectCall() const {
 
 bool CallBase::getCalleesMetadata(SmallVectorImpl<Function *> &Callees) const {
   Callees.clear();
-  if (!isIndirectCall())
-    return false;
-
   const MDNode *MD = getMetadata(LLVMContext::MD_callees);
   if (!MD)
     return false;
diff --git a/llvm/unittests/IR/InstructionsTest.cpp b/llvm/unittests/IR/InstructionsTest.cpp
index 900dbb00e26e2..398b89efa4586 100644
--- a/llvm/unittests/IR/InstructionsTest.cpp
+++ b/llvm/unittests/IR/InstructionsTest.cpp
@@ -174,13 +174,16 @@ TEST(InstructionsTest, CalleesMetadataDecoding) {
   MDNode *Valid = MDB.createCallees({Target0});
   Direct->setMetadata(LLVMContext::MD_callees, Valid);
   Callees.push_back(Target1);
-  EXPECT_FALSE(Direct->getCalleesMetadata(Callees));
-  EXPECT_TRUE(Callees.empty());
+  ASSERT_TRUE(Direct->getCalleesMetadata(Callees));
+  ASSERT_EQ(Callees.size(), 1u);
+  EXPECT_EQ(Callees[0], Target0);
 
+  // Decoding validates the attachment, not the identity of the called operand.
   InlineAsmCall->setMetadata(LLVMContext::MD_callees, Valid);
   Callees.push_back(Target1);
-  EXPECT_FALSE(InlineAsmCall->getCalleesMetadata(Callees));
-  EXPECT_TRUE(Callees.empty());
+  ASSERT_TRUE(InlineAsmCall->getCalleesMetadata(Callees));
+  ASSERT_EQ(Callees.size(), 1u);
+  EXPECT_EQ(Callees[0], Target0);
 
   Function *Invoker =
       Function::Create(CallerTy, GlobalValue::ExternalLinkage, "invoker", M);
@@ -199,6 +202,59 @@ TEST(InstructionsTest, CalleesMetadataDecoding) {
   EXPECT_EQ(Callees[1], Target0);
 }
 
+TEST(InstructionsTest, CalleesMetadataSurvivesCalleeSimplification) {
+  LLVMContext C;
+  Module M("test", C);
+  FunctionType *CalleeTy = FunctionType::get(Type::getVoidTy(C), false);
+  Function *Allowed =
+      Function::Create(CalleeTy, GlobalValue::ExternalLinkage, "allowed", M);
+  Function *Excluded =
+      Function::Create(CalleeTy, GlobalValue::ExternalLinkage, "excluded", M);
+  FunctionType *CallerTy =
+      FunctionType::get(Type::getVoidTy(C), PointerType::getUnqual(C), false);
+  Function *Caller =
+      Function::Create(CallerTy, GlobalValue::ExternalLinkage, "caller", M);
+  BasicBlock *Entry = BasicBlock::Create(C, "entry", Caller);
+  CallInst *Call = CallInst::Create(CalleeTy, Caller->getArg(0), {}, "", Entry);
+  ReturnInst::Create(C, Entry);
+
+  MDBuilder MDB(C);
+  MDNode *MD = MDB.createCallees({Allowed});
+  Call->setMetadata(LLVMContext::MD_callees, MD);
+  SmallVector<Function *, 4> Callees;
+  auto CheckList = [&] {
+    ASSERT_TRUE(Call->getCalleesMetadata(Callees));
+    ASSERT_EQ(Callees.size(), 1u);
+    EXPECT_EQ(Callees[0], Allowed);
+    EXPECT_EQ(Call->getMetadata(LLVMContext::MD_callees), MD);
+  };
+  ASSERT_TRUE(Call->isIndirectCall());
+  CheckList();
+
+  // A proven direct target in the list satisfies the unchanged constraint.
+  Call->setCalledOperand(Allowed);
+  ASSERT_FALSE(Call->isIndirectCall());
+  CheckList();
+
+  // Executing this call would be UB, but the attachment is still well-formed.
+  // The decoder must not discard it when simplification exposes the mismatch.
+  Call->setCalledOperand(Excluded);
+  CheckList();
+
+  // Other constant operands must not erase the constraint either.
+  Call->setCalledOperand(ConstantPointerNull::get(PointerType::getUnqual(C)));
+  CheckList();
+
+  // Empty is still distinct from absence after a call becomes direct.
+  Call->setCalledOperand(Allowed);
+  Call->setMetadata(LLVMContext::MD_callees, MDNode::get(C, {}));
+  EXPECT_TRUE(Call->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+  Call->setMetadata(LLVMContext::MD_callees, nullptr);
+  EXPECT_FALSE(Call->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+}
+
 TEST(InstructionsTest, CalleesMetadataRejectsMalformedOperands) {
   LLVMContext C;
   Module M("test", C);

>From 7d1f3eadd5e0281fe5489b6cde5f9cb1719963d3 Mon Sep 17 00:00:00 2001
From: mmiftahx <mmiftah.duna at gmail.com>
Date: Tue, 8 Sep 2026 01:09:41 -0500
Subject: [PATCH 3/3] fixup! [IR] Define empty and malformed !callees semantics

---
 llvm/docs/LangRef.md                   |  3 ++-
 llvm/include/llvm/IR/InstrTypes.h      | 13 +++++++------
 llvm/include/llvm/IR/MDBuilder.h       |  2 +-
 llvm/lib/IR/Instructions.cpp           |  2 ++
 llvm/unittests/IR/InstructionsTest.cpp | 11 +++++++----
 5 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 6145e5197fbd1..3474c53647a42 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7778,7 +7778,8 @@ Example (assuming 64-bit pointers):
 
 #### '`callees`' Metadata
 
-`callees` metadata may be attached to call sites.
+`callees` metadata may be attached to direct and indirect function call sites.
+It does not apply to inline assembly calls; attachments on those calls are ignored.
 Its operands provide an exhaustive list of possible callees.
 The list may be conservative: a listed function need not be dynamically feasible, but on every defined execution of the call the callee must be one of the listed functions.
 The order and duplication of operands are semantically irrelevant.
diff --git a/llvm/include/llvm/IR/InstrTypes.h b/llvm/include/llvm/IR/InstrTypes.h
index c83ea3a84a505..a0c7fc46801e3 100644
--- a/llvm/include/llvm/IR/InstrTypes.h
+++ b/llvm/include/llvm/IR/InstrTypes.h
@@ -1426,12 +1426,13 @@ class CallBase : public Instruction {
 
   /// Decode the exhaustive list of possible callees from !callees metadata.
   ///
-  /// Return true for a well-formed attachment, including an empty attachment.
-  /// Return false if the attachment is absent or contains an operand that is
-  /// not a Function. The result does not depend on the called operand and does
-  /// not check whether that operand satisfies the callee constraint.
-  /// \p Callees is cleared on failure. On success, duplicate functions are
-  /// omitted while preserving the order of their first occurrence.
+  /// Return true for a well-formed attachment on a function call, including an
+  /// empty attachment. Direct and indirect function calls are supported.
+  /// Return false for inline assembly calls, or if the attachment is absent or
+  /// contains an operand that is not a Function. This does not check whether
+  /// the called operand satisfies the callee constraint. \p Callees is cleared
+  /// on failure. On success, duplicate functions are omitted while preserving
+  /// the order of their first occurrence.
   LLVM_ABI bool getCalleesMetadata(SmallVectorImpl<Function *> &Callees) const;
 
   /// Determine whether the passed iterator points to the callee operand's Use.
diff --git a/llvm/include/llvm/IR/MDBuilder.h b/llvm/include/llvm/IR/MDBuilder.h
index 1a3a21405276f..fc7437c8ac626 100644
--- a/llvm/include/llvm/IR/MDBuilder.h
+++ b/llvm/include/llvm/IR/MDBuilder.h
@@ -132,7 +132,7 @@ class MDBuilder {
   //===------------------------------------------------------------------===//
 
   /// Return metadata indicating the exhaustive list of possible callees of
-  /// calls.
+  /// function calls.
   LLVM_ABI MDNode *createCallees(ArrayRef<Function *> Callees);
 
   //===------------------------------------------------------------------===//
diff --git a/llvm/lib/IR/Instructions.cpp b/llvm/lib/IR/Instructions.cpp
index afd1c917eb66c..92f662abe9278 100644
--- a/llvm/lib/IR/Instructions.cpp
+++ b/llvm/lib/IR/Instructions.cpp
@@ -344,6 +344,8 @@ bool CallBase::isIndirectCall() const {
 
 bool CallBase::getCalleesMetadata(SmallVectorImpl<Function *> &Callees) const {
   Callees.clear();
+  if (isInlineAsm())
+    return false;
   const MDNode *MD = getMetadata(LLVMContext::MD_callees);
   if (!MD)
     return false;
diff --git a/llvm/unittests/IR/InstructionsTest.cpp b/llvm/unittests/IR/InstructionsTest.cpp
index 398b89efa4586..5e124573d2909 100644
--- a/llvm/unittests/IR/InstructionsTest.cpp
+++ b/llvm/unittests/IR/InstructionsTest.cpp
@@ -178,12 +178,15 @@ TEST(InstructionsTest, CalleesMetadataDecoding) {
   ASSERT_EQ(Callees.size(), 1u);
   EXPECT_EQ(Callees[0], Target0);
 
-  // Decoding validates the attachment, not the identity of the called operand.
   InlineAsmCall->setMetadata(LLVMContext::MD_callees, Valid);
   Callees.push_back(Target1);
-  ASSERT_TRUE(InlineAsmCall->getCalleesMetadata(Callees));
-  ASSERT_EQ(Callees.size(), 1u);
-  EXPECT_EQ(Callees[0], Target0);
+  EXPECT_FALSE(InlineAsmCall->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
+
+  InlineAsmCall->setMetadata(LLVMContext::MD_callees, MDNode::get(C, {}));
+  Callees.push_back(Target1);
+  EXPECT_FALSE(InlineAsmCall->getCalleesMetadata(Callees));
+  EXPECT_TRUE(Callees.empty());
 
   Function *Invoker =
       Function::Create(CallerTy, GlobalValue::ExternalLinkage, "invoker", M);



More information about the llvm-commits mailing list