[llvm] [IR] Add nofreeobj parameter/return attribute (PR #206445)
Nikita Popov via llvm-commits
llvm-commits at lists.llvm.org
Thu Jul 16 06:01:26 PDT 2026
https://github.com/nikic updated https://github.com/llvm/llvm-project/pull/206445
>From 7f4711b473f43361e7fde96cd685fba15e4d30f8 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 11:43:09 +0200
Subject: [PATCH 1/4] Introduce nofreeobj
---
llvm/docs/LangRef.md | 22 ++++++++++++++++++-
llvm/include/llvm/Bitcode/LLVMBitCodes.h | 1 +
llvm/include/llvm/IR/Attributes.td | 4 ++++
llvm/lib/Bitcode/Reader/BitcodeReader.cpp | 2 ++
llvm/lib/Bitcode/Writer/BitcodeWriter.cpp | 2 ++
llvm/lib/IR/Value.cpp | 9 ++++++++
llvm/lib/Transforms/Utils/CodeExtractor.cpp | 1 +
.../ValueTracking/memory-dereferenceable.ll | 17 ++++++++++++++
llvm/test/Bitcode/attributes.ll | 5 +++++
9 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index f555ca543ffdc..8c563cbd079c0 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -1587,7 +1587,27 @@ Currently, only the following parameter attributes are defined:
This is not a valid attribute for return values.
-(nest)=
+`nofreeobj`
+: On arguments, this indicates that the underlying object of the argument
+ cannot be freed during the execution of the function (where "during" is in
+ the same sense as for `nofree`).
+
+ On return values, this indicates that the underlying object of the return
+ value cannot be freed from this point forward, for the duration of the
+ program's execution.
+
+ Unlike `nofree`, it is not possible to free the underlying object through
+ a different pointer either.
+
+ `nofreeobj` on arguments implies `nofree`. `nofreeobj` is primarily useful
+ in cases where it's not possible to mark the argument as `noalias`.
+ The combination of `nofree` and `noalias` does *not* imply `nofreeobj`
+ by itself, because it still allows freeing the underlying object through
+ a different pointer, as long as the object is never accessed through the
+ `noalias` pointer. However, it does imply `nofreeobj` if such an access
+ exists, for example in the form of the implied read of a `dereferenceable`
+ attribute.
+
`nest`
: This indicates that the pointer parameter can be excised using the
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index 358f9a65a80af..226be19624db0 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -826,6 +826,7 @@ enum AttributeKindCodes {
ATTR_KIND_NOOUTLINE = 107,
ATTR_KIND_FLATTEN = 108,
ATTR_KIND_NOIPA = 109,
+ ATTR_KIND_NOFREEOBJ = 110,
};
enum ComdatSelectionKindCodes {
diff --git a/llvm/include/llvm/IR/Attributes.td b/llvm/include/llvm/IR/Attributes.td
index 4e45100b54d38..ea523cad69cd5 100644
--- a/llvm/include/llvm/IR/Attributes.td
+++ b/llvm/include/llvm/IR/Attributes.td
@@ -201,8 +201,12 @@ def NoDuplicate : EnumAttr<"noduplicate", IntersectPreserve, [FnAttr]>;
def NoExt : EnumAttr<"noext", IntersectPreserve, [ParamAttr, RetAttr]>;
/// Function does not deallocate memory.
+/// Argument cannot be freed based on the argument pointer.
def NoFree : EnumAttr<"nofree", IntersectAnd, [FnAttr, ParamAttr]>;
+/// Underlying object of argument/return cannot be freed.
+def NoFreeObj : EnumAttr<"nofreeobj", IntersectAnd, [ParamAttr, RetAttr]>;
+
/// Argument is dead if the call unwinds.
def DeadOnUnwind : EnumAttr<"dead_on_unwind", IntersectAnd, [ParamAttr]>;
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index c57323ed41525..aaccc9de11d49 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2176,6 +2176,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
return Attribute::NoDuplicate;
case bitc::ATTR_KIND_NOFREE:
return Attribute::NoFree;
+ case bitc::ATTR_KIND_NOFREEOBJ:
+ return Attribute::NoFreeObj;
case bitc::ATTR_KIND_NO_IMPLICIT_FLOAT:
return Attribute::NoImplicitFloat;
case bitc::ATTR_KIND_NO_INLINE:
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index f4ce522c23b8e..9378846900570 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -866,6 +866,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
return bitc::ATTR_KIND_NO_DUPLICATE;
case Attribute::NoFree:
return bitc::ATTR_KIND_NOFREE;
+ case Attribute::NoFreeObj:
+ return bitc::ATTR_KIND_NOFREEOBJ;
case Attribute::NoImplicitFloat:
return bitc::ATTR_KIND_NO_IMPLICIT_FLOAT;
case Attribute::NoInline:
diff --git a/llvm/lib/IR/Value.cpp b/llvm/lib/IR/Value.cpp
index 6d0170dd23e75..368d6241a48ca 100644
--- a/llvm/lib/IR/Value.cpp
+++ b/llvm/lib/IR/Value.cpp
@@ -861,12 +861,21 @@ bool Value::canBeFreed() const {
// another pointer to the same allocation. Readonly implies nofree.
if ((A->hasNoFreeAttr() || A->onlyReadsMemory()) && A->hasNoAliasAttr())
return false;
+
+ // nofreeobj means that the underlying object cannot be freed, even
+ // through a different pointer.
+ if (A->hasAttribute(Attribute::NoFreeObj))
+ return false;
}
if (auto *ITP = dyn_cast<IntToPtrInst>(this);
ITP && ITP->hasMetadata(LLVMContext::MD_nofree))
return false;
+ if (auto *CB = dyn_cast<CallBase>(this))
+ if (CB->hasRetAttr(Attribute::NoFreeObj))
+ return false;
+
const Function *F = nullptr;
if (auto *I = dyn_cast<Instruction>(this))
F = I->getFunction();
diff --git a/llvm/lib/Transforms/Utils/CodeExtractor.cpp b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
index 4c33848dacf51..745f47aa561ee 100644
--- a/llvm/lib/Transforms/Utils/CodeExtractor.cpp
+++ b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
@@ -1052,6 +1052,7 @@ Function *CodeExtractor::constructFunctionDeclaration(
case Attribute::Range:
case Attribute::Initializes:
case Attribute::NoExt:
+ case Attribute::NoFreeObj:
// These are not really attributes.
case Attribute::None:
case Attribute::EndAttrKinds:
diff --git a/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll b/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
index f7e69b0c1f04f..09c8e74a5c675 100644
--- a/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
+++ b/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
@@ -301,6 +301,23 @@ define void @infer_missing_noalias2(ptr dereferenceable(8) readonly %p) {
ret void
}
+; CHECK-LABEL: 'nofreeobj_arg'
+; CHECK: %p
+define void @nofreeobj_arg(ptr dereferenceable(8) nofreeobj %p) {
+ call void @mayfree()
+ %v = load i32, ptr %p
+ ret void
+}
+
+; CHECK-LABEL: 'nofreeobj_ret'
+; CHECK: %p
+define void @nofreeobj_ret() {
+ %p = call dereferenceable(8) nofreeobj ptr @foo()
+ call void @mayfree()
+ %v = load i32, ptr %p
+ ret void
+}
+
; Just check that we don't crash.
; CHECK-LABEL: 'opaque_type_crasher'
define void @opaque_type_crasher(ptr dereferenceable(16) %a, i1 %arg) {
diff --git a/llvm/test/Bitcode/attributes.ll b/llvm/test/Bitcode/attributes.ll
index f696f2dd12323..3d5272f053251 100644
--- a/llvm/test/Bitcode/attributes.ll
+++ b/llvm/test/Bitcode/attributes.ll
@@ -597,6 +597,11 @@ define void @noipa() noipa {
ret void
}
+; CHECK: define nofreeobj ptr @nofreeobj(ptr nofreeobj %p)
+define nofreeobj ptr @nofreeobj(ptr nofreeobj %p) {
+ ret ptr %p
+}
+
; CHECK: attributes #0 = { noreturn }
; CHECK: attributes #1 = { nounwind }
; CHECK: attributes #2 = { memory(none) }
>From aae6e38fc73bf74a23578ed9d580e76ea4a96df9 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 11:53:25 +0200
Subject: [PATCH 2/4] Verify type for nofree/nofreeobj
---
llvm/lib/IR/Attributes.cpp | 4 +++-
llvm/test/Verifier/nofree.ll | 16 ++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 llvm/test/Verifier/nofree.ll
diff --git a/llvm/lib/IR/Attributes.cpp b/llvm/lib/IR/Attributes.cpp
index 4087b25951a1c..e69b848c0eb66 100644
--- a/llvm/lib/IR/Attributes.cpp
+++ b/llvm/lib/IR/Attributes.cpp
@@ -2520,7 +2520,9 @@ AttributeMask AttributeFuncs::typeIncompatible(Type *Ty, AttributeSet AS,
.addAttribute(Attribute::DeadOnUnwind)
.addAttribute(Attribute::Initializes)
.addAttribute(Attribute::Captures)
- .addAttribute(Attribute::DeadOnReturn);
+ .addAttribute(Attribute::DeadOnReturn)
+ .addAttribute(Attribute::NoFree)
+ .addAttribute(Attribute::NoFreeObj);
if (ASK & ASK_UNSAFE_TO_DROP)
Incompatible.addAttribute(Attribute::Nest)
.addAttribute(Attribute::SwiftError)
diff --git a/llvm/test/Verifier/nofree.ll b/llvm/test/Verifier/nofree.ll
new file mode 100644
index 0000000000000..2775613c75426
--- /dev/null
+++ b/llvm/test/Verifier/nofree.ll
@@ -0,0 +1,16 @@
+; RUN: not opt -S < %s 2>&1 | FileCheck %s
+
+; CHECK: Attribute 'nofree' applied to incompatible type!
+define void @test(i32 nofree %p) {
+ ret void
+}
+
+; CHECK: Attribute 'nofreeobj' applied to incompatible type!
+define void @test2(i32 nofreeobj %p) {
+ ret void
+}
+
+; CHECK: Attribute 'nofreeobj' applied to incompatible type!
+define nofreeobj i32 @test3() {
+ ret i32 0
+}
>From 834d54468d47a3eeb56812bb67ba72fa51e891f9 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 11:54:08 +0200
Subject: [PATCH 3/4] Rename !nofree to !nofreeobj
---
llvm/docs/LangRef.md | 9 +++++----
llvm/include/llvm/IR/FixedMetadataKinds.def | 2 +-
llvm/lib/IR/Value.cpp | 2 +-
llvm/lib/IR/Verifier.cpp | 15 ++++++++-------
.../Transforms/LICM/hoist-speculatable-load.ll | 4 ++--
llvm/test/Verifier/nofree_metadata.ll | 8 ++++----
6 files changed, 21 insertions(+), 19 deletions(-)
diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 8c563cbd079c0..3c717938fc14e 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -12928,7 +12928,7 @@ address width of 32 bits (`p1:64:64:64:32` {ref}`datalayout string<langref_datal
##### Syntax:
```
-<result> = inttoptr <ty> <value> to <ty2>[, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !nofree !<empty_node>] ; yields ty2
+<result> = inttoptr <ty> <value> to <ty2>[, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !nofreeobj !<empty_node>] ; yields ty2
```
##### Overview:
@@ -12952,10 +12952,11 @@ metadata name `<deref_bytes_node>` corresponding to a metadata node with one
`i64` entry.
See `dereferenceable_or_null` metadata.
-The optional `!nofree` metadata must reference a single metadata name
+The optional `!nofreeobj` metadata must reference a single metadata name
`<empty_node>` corresponding to a metadata node with no entries.
-The existence of the `!nofree` metadata on the instruction tells the optimizer
-that the memory pointed by the pointer will not be freed after this point.
+The existence of the `!nofreeobj` metadata on the instruction tells the
+optimizer that the memory pointed by the pointer will not be freed after this
+point.
##### Semantics:
diff --git a/llvm/include/llvm/IR/FixedMetadataKinds.def b/llvm/include/llvm/IR/FixedMetadataKinds.def
index d10d16075fecb..65c488f50064c 100644
--- a/llvm/include/llvm/IR/FixedMetadataKinds.def
+++ b/llvm/include/llvm/IR/FixedMetadataKinds.def
@@ -54,7 +54,7 @@ LLVM_FIXED_MD_KIND(MD_coro_outside_frame, "coro.outside.frame", 39)
LLVM_FIXED_MD_KIND(MD_mmra, "mmra", 40)
LLVM_FIXED_MD_KIND(MD_noalias_addrspace, "noalias.addrspace", 41)
LLVM_FIXED_MD_KIND(MD_callee_type, "callee_type", 42)
-LLVM_FIXED_MD_KIND(MD_nofree, "nofree", 43)
+LLVM_FIXED_MD_KIND(MD_nofreeobj, "nofreeobj", 43)
LLVM_FIXED_MD_KIND(MD_captures, "captures", 44)
LLVM_FIXED_MD_KIND(MD_alloc_token, "alloc_token", 45)
LLVM_FIXED_MD_KIND(MD_implicit_ref, "implicit.ref", 46)
diff --git a/llvm/lib/IR/Value.cpp b/llvm/lib/IR/Value.cpp
index 368d6241a48ca..50882ec0d7b10 100644
--- a/llvm/lib/IR/Value.cpp
+++ b/llvm/lib/IR/Value.cpp
@@ -869,7 +869,7 @@ bool Value::canBeFreed() const {
}
if (auto *ITP = dyn_cast<IntToPtrInst>(this);
- ITP && ITP->hasMetadata(LLVMContext::MD_nofree))
+ ITP && ITP->hasMetadata(LLVMContext::MD_nofreeobj))
return false;
if (auto *CB = dyn_cast<CallBase>(this))
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index fa0fe7a2e6092..09949bc8cff42 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -357,7 +357,7 @@ class Verifier : public InstVisitor<Verifier>, VerifierSupport {
void visitNoFPClassMetadata(Instruction &I, MDNode *Range, Type *Ty);
void visitNoaliasAddrspaceMetadata(Instruction &I, MDNode *Range, Type *Ty);
void visitDereferenceableMetadata(Instruction &I, MDNode *MD);
- void visitNofreeMetadata(Instruction &I, MDNode *MD);
+ void visitNoFreeObjMetadata(Instruction &I, MDNode *MD);
void visitProfMetadata(Instruction &I, MDNode *MD);
void visitCallStackMetadata(MDNode *MD);
void visitMemProfMetadata(Instruction &I, MDNode *MD);
@@ -5229,11 +5229,12 @@ void Verifier::visitDereferenceableMetadata(Instruction& I, MDNode* MD) {
&I);
}
-void Verifier::visitNofreeMetadata(Instruction &I, MDNode *MD) {
- Check(I.getType()->isPointerTy(), "nofree applies only to pointer types", &I);
- Check((isa<IntToPtrInst>(I)), "nofree applies only to inttoptr instruction",
+void Verifier::visitNoFreeObjMetadata(Instruction &I, MDNode *MD) {
+ Check(I.getType()->isPointerTy(), "nofreeobj applies only to pointer types",
&I);
- Check(MD->getNumOperands() == 0, "nofree metadata must be empty", &I);
+ Check((isa<IntToPtrInst>(I)),
+ "nofreeobj applies only to inttoptr instruction", &I);
+ Check(MD->getNumOperands() == 0, "nofreeobj metadata must be empty", &I);
}
void Verifier::visitProfMetadata(Instruction &I, MDNode *MD) {
@@ -5842,8 +5843,8 @@ void Verifier::visitInstruction(Instruction &I) {
if (MDNode *MD = I.getMetadata(LLVMContext::MD_dereferenceable_or_null))
visitDereferenceableMetadata(I, MD);
- if (MDNode *MD = I.getMetadata(LLVMContext::MD_nofree))
- visitNofreeMetadata(I, MD);
+ if (MDNode *MD = I.getMetadata(LLVMContext::MD_nofreeobj))
+ visitNoFreeObjMetadata(I, MD);
if (MDNode *TBAA = I.getMetadata(LLVMContext::MD_tbaa))
TBAAVerifyHelper.visitTBAAMetadata(&I, TBAA);
diff --git a/llvm/test/Transforms/LICM/hoist-speculatable-load.ll b/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
index 31236e8f29d60..ab754a1f9a9d6 100644
--- a/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
+++ b/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
@@ -4,7 +4,7 @@
define void @f(i32 %ptr_i, ptr %ptr2, i1 %cond) {
; CHECK-LABEL: @f(
; CHECK-NEXT: entry:
-; CHECK-NEXT: [[PTR:%.*]] = inttoptr i32 [[PTR_I:%.*]] to ptr, !nofree [[META0:![0-9]+]]
+; CHECK-NEXT: [[PTR:%.*]] = inttoptr i32 [[PTR_I:%.*]] to ptr, !nofreeobj [[META0:![0-9]+]]
; CHECK-NEXT: call void @llvm.assume(i1 true) [ "align"(ptr [[PTR]], i32 16), "dereferenceable"(ptr [[PTR]], i32 16) ]
; CHECK-NEXT: br i1 [[COND:%.*]], label [[FOR_BODY_LR_PH:%.*]], label [[IF0:%.*]]
; CHECK: if0:
@@ -27,7 +27,7 @@ define void @f(i32 %ptr_i, ptr %ptr2, i1 %cond) {
; CHECK-NEXT: ret void
;
entry:
- %ptr = inttoptr i32 %ptr_i to ptr, !nofree !{}
+ %ptr = inttoptr i32 %ptr_i to ptr, !nofreeobj !{}
call void @llvm.assume(i1 true) [ "align"(ptr %ptr, i32 16), "dereferenceable"(ptr %ptr, i32 16) ]
br i1 %cond, label %for.body.lr.ph, label %if0
diff --git a/llvm/test/Verifier/nofree_metadata.ll b/llvm/test/Verifier/nofree_metadata.ll
index e04f5b9f1c522..40a4d3e28bcd1 100644
--- a/llvm/test/Verifier/nofree_metadata.ll
+++ b/llvm/test/Verifier/nofree_metadata.ll
@@ -2,14 +2,14 @@
declare ptr @dummy()
-; CHECK: nofree applies only to inttoptr instruction
+; CHECK: nofreeobj applies only to inttoptr instruction
define void @test_not_inttoptr() {
- call ptr @dummy(), !nofree !{}
+ call ptr @dummy(), !nofreeobj !{}
ret void
}
-; CHECK: nofree metadata must be empty
+; CHECK: nofreeobj metadata must be empty
define void @test_invalid_arg(i32 %p) {
- inttoptr i32 %p to ptr, !nofree !{i32 0}
+ inttoptr i32 %p to ptr, !nofreeobj !{i32 0}
ret void
}
>From 3505f2f7bbadac2a674bc91bab212cfc7ab55033 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 12:03:19 +0200
Subject: [PATCH 4/4] Release note
---
llvm/docs/ReleaseNotes.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index af037473e3653..6c53cf2cc7e2d 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -54,6 +54,11 @@ Makes programs 10x faster by doing Special New Thing.
### Changes to the LLVM IR
+* Added `nofreeobj` attribute for attributes and returns, which forbids
+ freeing the underlying object (as opposed to only frees through that specific
+ pointer). Renamed `!nofree` metadata to `!nofreeobj`, as it has the same
+ semantics.
+
### Changes to LLVM infrastructure
### Changes to building LLVM
More information about the llvm-commits
mailing list