[llvm] [IR] Add writerange attribute conveying a pointer argument's write range in a function (PR #227191)

Kunal Chadha via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 28 22:49:33 PDT 2026


https://github.com/chadhakunal created https://github.com/llvm/llvm-project/pull/227191

Adds a new pointer parameter attribute, `writerange((Lo,Hi), ...)`, that bounds the byte ranges a function may write to. This lets `getModRefInfo` prove `NoModRef` for a location a call provably never touches, letting GVN eliminate a now-redundant load.

Fixes #221792

>From 61daeb9c8e2d0aa114220e4a06a3674026cc5834 Mon Sep 17 00:00:00 2001
From: Kunal Chadha <kunachad at qti.qualcomm.com>
Date: Mon, 28 Sep 2026 22:45:00 -0700
Subject: [PATCH] [IR] Add writerange attribute conveying a pointer argument's
 write range in a function

---
 llvm/include/llvm/AsmParser/LLParser.h      |  1 +
 llvm/include/llvm/Bitcode/LLVMBitCodes.h    |  1 +
 llvm/include/llvm/IR/Attributes.h           |  6 +++
 llvm/include/llvm/IR/Attributes.td          |  3 ++
 llvm/lib/Analysis/BasicAliasAnalysis.cpp    | 45 +++++++++++++++++++-
 llvm/lib/AsmParser/LLParser.cpp             | 47 +++++++++++++++++++++
 llvm/lib/Bitcode/Reader/BitcodeReader.cpp   |  2 +
 llvm/lib/Bitcode/Writer/BitcodeWriter.cpp   |  2 +
 llvm/lib/IR/Attributes.cpp                  | 20 +++++++++
 llvm/lib/IR/Verifier.cpp                    | 10 +++++
 llvm/lib/Transforms/IPO/FunctionAttrs.cpp   | 35 +++++++++++++++
 llvm/lib/Transforms/Utils/CodeExtractor.cpp |  1 +
 llvm/test/Transforms/GVN/writerange.ll      | 34 +++++++++++++++
 13 files changed, 205 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/Transforms/GVN/writerange.ll

diff --git a/llvm/include/llvm/AsmParser/LLParser.h b/llvm/include/llvm/AsmParser/LLParser.h
index 70b98dae850fd2..b0fd3a26c1157a 100644
--- a/llvm/include/llvm/AsmParser/LLParser.h
+++ b/llvm/include/llvm/AsmParser/LLParser.h
@@ -414,6 +414,7 @@ namespace llvm {
                                     bool inAttrGrp, LocTy &BuiltinLoc);
     bool parseRangeAttr(AttrBuilder &B);
     bool parseInitializesAttr(AttrBuilder &B);
+    bool parseWriteRangeAttr(AttrBuilder &B);
     bool parseCapturesAttr(AttrBuilder &B);
     bool parseRequiredTypeAttr(AttrBuilder &B, lltok::Kind AttrToken,
                                Attribute::AttrKind AttrKind);
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index d9ae1d0c195a63..f411cd96f6361c 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -833,6 +833,7 @@ enum AttributeKindCodes {
   ATTR_KIND_FLATTEN = 108,
   ATTR_KIND_NOIPA = 109,
   ATTR_KIND_NOFREEOBJ = 110,
+  ATTR_KIND_WRITE_RANGE = 111,
 };
 
 enum ComdatSelectionKindCodes {
diff --git a/llvm/include/llvm/IR/Attributes.h b/llvm/include/llvm/IR/Attributes.h
index 7ff7def7525fa2..b704bf7fb7d01b 100644
--- a/llvm/include/llvm/IR/Attributes.h
+++ b/llvm/include/llvm/IR/Attributes.h
@@ -363,6 +363,9 @@ class Attribute {
   /// Returns the value of the initializes attribute.
   LLVM_ABI ArrayRef<ConstantRange> getInitializes() const;
 
+  /// Returns the value of the writerange attribute.
+  LLVM_ABI ArrayRef<ConstantRange> getWriteRange() const;
+
   /// The Attribute is converted to a string of equivalent mnemonic. This
   /// is, presumably, for writing out the mnemonics for the assembly writer.
   LLVM_ABI std::string getAsString(bool InAttrGrp = false) const;
@@ -1370,6 +1373,9 @@ class AttrBuilder {
   /// Add initializes attribute.
   LLVM_ABI AttrBuilder &addInitializesAttr(const ConstantRangeList &CRL);
 
+  /// Add writerange attribute.
+  LLVM_ABI AttrBuilder &addWriteRangeAttr(const ConstantRangeList &CRL);
+
   /// Add 0 or more parameter attributes which are equivalent to metadata
   /// attached to \p I. e.g. !align -> align. This assumes the argument type is
   /// the same as the original instruction and the attribute is compatible.
diff --git a/llvm/include/llvm/IR/Attributes.td b/llvm/include/llvm/IR/Attributes.td
index c43b499061ddc7..937f1677a60cfa 100644
--- a/llvm/include/llvm/IR/Attributes.td
+++ b/llvm/include/llvm/IR/Attributes.td
@@ -151,6 +151,9 @@ def InAlloca : TypeAttr<"inalloca", IntersectPreserve, [ParamAttr, ABIAttr]>;
 /// Pointer argument memory is initialized.
 def Initializes : ConstantRangeListAttr<"initializes", IntersectPreserve, [ParamAttr]>;
 
+/// Pointer argument memory is written to.
+def WriteRange : ConstantRangeListAttr<"writerange", IntersectPreserve, [ParamAttr]>;
+
 /// Source said inlining was desirable.
 def InlineHint : EnumAttr<"inlinehint", IntersectAnd, [FnAttr]>;
 
diff --git a/llvm/lib/Analysis/BasicAliasAnalysis.cpp b/llvm/lib/Analysis/BasicAliasAnalysis.cpp
index 3aece3ca7d7695..91a419c235406a 100644
--- a/llvm/lib/Analysis/BasicAliasAnalysis.cpp
+++ b/llvm/lib/Analysis/BasicAliasAnalysis.cpp
@@ -942,6 +942,38 @@ AliasResult BasicAAResult::alias(const MemoryLocation &LocA,
   return aliasCheck(LocA.Ptr, LocA.Size, LocB.Ptr, LocB.Size, AAQI, CtxI);
 }
 
+static bool ArgWriteRangeDisjointFromLoc(const CallBase *Call,
+                                              unsigned ArgIdx,
+                                              const Value *Arg,
+                                              const MemoryLocation &Loc,
+                                              const DataLayout &DL) {
+  if (!Call->paramHasAttr(ArgIdx, Attribute::WriteRange))
+    return false;
+  if (!Loc.Size.hasValue() || !Loc.Size.isPrecise() ||
+      Loc.Size.getValue().isScalable())
+    return false;
+
+  unsigned IndexWidth = DL.getIndexTypeSizeInBits(Arg->getType());
+  APInt LocOffset(IndexWidth, 0);
+  const Value *LocBase = Loc.Ptr->stripAndAccumulateConstantOffsets(
+      DL, LocOffset, /*AllowNonInbounds=*/true);
+  if (LocBase != Arg)
+    return false;
+
+  int64_t Lo = LocOffset.getSExtValue();
+  int64_t Hi = Lo + static_cast<int64_t>(Loc.Size.getValue().getFixedValue());
+
+  ArrayRef<ConstantRange> Ranges =
+      Call->getParamAttr(ArgIdx, Attribute::WriteRange).getWriteRange();
+  for (const ConstantRange &CR : Ranges) {
+    int64_t RLo = CR.getLower().getSExtValue();
+    int64_t RHi = CR.getUpper().getSExtValue();
+    if (Lo < RHi && RLo < Hi)
+      return false; // Overlaps; cannot rule out.
+  }
+  return true; // Disjoint from every declared write range.
+}
+
 /// Checks to see if the specified callsite can clobber the specified memory
 /// object.
 ///
@@ -1031,8 +1063,16 @@ ModRefInfo BasicAAResult::getModRefInfo(const CallBase *Call,
               ? MemoryLocation::getForArgument(Call, ArgIdx, TLI)
               : MemoryLocation::getBeforeOrAfter(Arg);
       AliasResult ArgAlias = AAQI.AAR.alias(ArgLoc, Loc, AAQI, Call);
-      if (ArgAlias != AliasResult::NoAlias)
-        NewArgMR |= ArgMR & AAQI.AAR.getArgModRefInfo(Call, ArgIdx);
+      
+      if (ArgAlias != AliasResult::NoAlias) {
+        ModRefInfo PerArgMR = ArgMR & AAQI.AAR.getArgModRefInfo(Call, ArgIdx);
+
+        if (Call->isArgOperand(&U) && isModSet(PerArgMR) &&
+            ArgWriteRangeDisjointFromLoc(Call, ArgIdx, Arg, Loc, DL))
+          PerArgMR &= ~ModRefInfo::Mod;
+
+        NewArgMR |= PerArgMR;
+      }
 
       // Exit early if we cannot improve over the original ArgMR.
       if (NewArgMR == ArgMR)
@@ -1551,6 +1591,7 @@ AliasResult BasicAAResult::aliasCheck(const Value *V1, LocationSize V1Size,
                                       const Instruction *CtxI) {
   // If either of the memory references is empty, it doesn't matter what the
   // pointer values are.
+
   if (V1Size.isZero() || V2Size.isZero())
     return AliasResult::NoAlias;
 
diff --git a/llvm/lib/AsmParser/LLParser.cpp b/llvm/lib/AsmParser/LLParser.cpp
index aaee06685fc2ad..528b7cbe98aa8a 100644
--- a/llvm/lib/AsmParser/LLParser.cpp
+++ b/llvm/lib/AsmParser/LLParser.cpp
@@ -1806,6 +1806,8 @@ bool LLParser::parseEnumAttribute(Attribute::AttrKind Attr, AttrBuilder &B,
     return parseRangeAttr(B);
   case Attribute::Initializes:
     return parseInitializesAttr(B);
+  case Attribute::WriteRange:
+    return parseWriteRangeAttr(B);
   case Attribute::Captures:
     return parseCapturesAttr(B);
   default:
@@ -3541,6 +3543,51 @@ bool LLParser::parseInitializesAttr(AttrBuilder &B) {
   return false;
 }
 
+/// parseWriteRangeAttr
+///   ::= writerange((Lo1,Hi1),(Lo2,Hi2),...)
+bool LLParser::parseWriteRangeAttr(AttrBuilder &B) {
+  Lex.Lex();
+
+  auto ParseAPSInt = [&](APInt &Val) {
+    if (Lex.getKind() != lltok::APSInt)
+      return tokError("expected integer");
+    Val = Lex.getAPSIntVal().extend(64);
+    Lex.Lex();
+    return false;
+  };
+
+  if (parseToken(lltok::lparen, "expected '('"))
+    return true;
+
+  SmallVector<ConstantRange, 2> RangeList;
+  do {
+    APInt Lower, Upper;
+    if (parseToken(lltok::lparen, "expected '('"))
+      return true;
+
+    if (ParseAPSInt(Lower) || parseToken(lltok::comma, "expected ','") ||
+        ParseAPSInt(Upper))
+      return true;
+
+    if (Lower == Upper)
+      return tokError("the range should not represent the full or empty set!");
+
+    if (parseToken(lltok::rparen, "expected ')'"))
+      return true;
+
+    RangeList.push_back(ConstantRange(Lower, Upper));
+  } while (EatIfPresent(lltok::comma));
+
+  if (parseToken(lltok::rparen, "expected ')'"))
+    return true;
+
+  auto CRLOrNull = ConstantRangeList::getConstantRangeList(RangeList);
+  if (!CRLOrNull.has_value())
+    return tokError("Invalid (unordered or overlapping) range list");
+  B.addWriteRangeAttr(*CRLOrNull);
+  return false;
+}
+
 bool LLParser::parseCapturesAttr(AttrBuilder &B) {
   CaptureComponents Other = CaptureComponents::None;
   std::optional<CaptureComponents> Ret;
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index 4b338ae6237ecf..115ff2870f4edd 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2322,6 +2322,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
     return Attribute::Range;
   case bitc::ATTR_KIND_INITIALIZES:
     return Attribute::Initializes;
+  case bitc::ATTR_KIND_WRITE_RANGE:
+    return Attribute::WriteRange;
   case bitc::ATTR_KIND_CORO_ELIDE_SAFE:
     return Attribute::CoroElideSafe;
   case bitc::ATTR_KIND_NO_EXT:
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index 34e62d2d8f7d0e..6365fc9490e74e 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -1004,6 +1004,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
     return bitc::ATTR_KIND_RANGE;
   case Attribute::Initializes:
     return bitc::ATTR_KIND_INITIALIZES;
+  case Attribute::WriteRange:
+    return bitc::ATTR_KIND_WRITE_RANGE;
   case Attribute::NoExt:
     return bitc::ATTR_KIND_NO_EXT;
   case Attribute::Captures:
diff --git a/llvm/lib/IR/Attributes.cpp b/llvm/lib/IR/Attributes.cpp
index 7f5f5f34af3899..f0ede4d02feda8 100644
--- a/llvm/lib/IR/Attributes.cpp
+++ b/llvm/lib/IR/Attributes.cpp
@@ -522,6 +522,12 @@ ArrayRef<ConstantRange> Attribute::getInitializes() const {
   return pImpl->getValueAsConstantRangeList();
 }
 
+ArrayRef<ConstantRange> Attribute::getWriteRange() const {
+  assert(hasAttribute(Attribute::WriteRange) &&
+         "Trying to get writerange attr from non-ConstantRangeList attribute");
+  return pImpl->getValueAsConstantRangeList();
+}
+
 static const char *getModRefStr(ModRefInfo MR) {
   switch (MR) {
   case ModRefInfo::NoModRef:
@@ -736,6 +742,16 @@ std::string Attribute::getAsString(bool InAttrGrp) const {
     return Result;
   }
 
+  if (hasAttribute(Attribute::WriteRange)) {
+    std::string Result;
+    raw_string_ostream OS(Result);
+    ConstantRangeList CRL = getWriteRange();
+    OS << "writerange(";
+    CRL.print(OS);
+    OS << ")";
+    return Result;
+  }
+
   // Convert target-dependent attributes to strings of the form:
   //
   //   "kind"
@@ -2357,6 +2373,10 @@ AttrBuilder &AttrBuilder::addInitializesAttr(const ConstantRangeList &CRL) {
   return addConstantRangeListAttr(Attribute::Initializes, CRL.rangesRef());
 }
 
+AttrBuilder &AttrBuilder::addWriteRangeAttr(const ConstantRangeList &CRL) {
+  return addConstantRangeListAttr(Attribute::WriteRange, CRL.rangesRef());
+}
+
 AttrBuilder &AttrBuilder::addFromEquivalentMetadata(const Instruction &I) {
   if (I.hasMetadata(LLVMContext::MD_nonnull))
     addAttribute(Attribute::NonNull);
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 2502d574dd9928..11f999fe485728 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -2392,6 +2392,16 @@ void Verifier::verifyParameterAttrs(AttributeSet Attrs, Type *Ty,
           "Attribute 'initializes' does not support unordered ranges", V);
   }
 
+  if (Attrs.hasAttribute(Attribute::WriteRange)) {
+    auto Ranges = Attrs.getAttribute(Attribute::WriteRange).getWriteRange();
+    Check(!Ranges.empty(), "Attribute 'writerange' does not support empty list",
+          V);
+    Check(ConstantRangeList::isOrderedRanges(Ranges),
+          "Attribute 'writerange' does not support unordered ranges", V);
+    Check(!Attrs.hasAttribute(Attribute::ReadNone),
+          "Attribute 'writerange' is incompatible with 'readnone'", V);
+  }
+
   if (Attrs.hasAttribute(Attribute::NoFPClass)) {
     uint64_t Val = Attrs.getAttribute(Attribute::NoFPClass).getValueAsInt();
     Check(Val != 0, "Attribute 'nofpclass' must have at least one test bit set",
diff --git a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
index a713ead683476e..33b4ec901713a6 100644
--- a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
@@ -1115,6 +1115,35 @@ static bool addAccessAttrs(Argument *A, ArgAccessProperties Props) {
   return true;
 }
 
+static bool inferWriteRange(Argument &A, Function &F) {
+  auto ArgumentUses = collectArgumentUsesPerBlock(A, F);
+
+  ConstantRangeList WriteCRL;
+  for (auto &BBInfo : llvm::make_second_range(ArgumentUses.UsesPerBlock)) {
+    for (auto &Info : llvm::make_second_range(BBInfo.Insts)) {
+      if (Info.ArgAccessType == ArgumentAccessInfo::AccessType::Write)
+        WriteCRL = WriteCRL.unionWith(Info.AccessRanges);
+      else if (Info.ArgAccessType != ArgumentAccessInfo::AccessType::Read)
+        return false; // Unknown or WriteWithSideEffect: can't bound this arg.
+    }
+  }
+
+  if (WriteCRL.empty())
+    return false;
+
+  if (A.hasAttribute(Attribute::WriteRange)) {
+    ConstantRangeList PreviousCRL(
+        A.getAttribute(Attribute::WriteRange).getWriteRange());
+    if (PreviousCRL == WriteCRL)
+      return false;
+    WriteCRL = WriteCRL.unionWith(PreviousCRL);
+  }
+
+  A.addAttr(Attribute::get(A.getContext(), Attribute::WriteRange,
+                           WriteCRL.rangesRef()));
+  return true;
+}
+
 static bool inferInitializes(Argument &A, Function &F) {
   auto ArgumentUses = collectArgumentUsesPerBlock(A, F);
   // No write anywhere in the function, bail.
@@ -1311,6 +1340,12 @@ static void addArgumentAttrs(const SCCNodeSet &SCCNodes,
         if (DetermineAccessAttrsForSingleton(&A))
           Changed.insert(F);
       }
+
+      if (!A.onlyReadsMemory()) {
+        if(inferWriteRange(A, *F))
+          Changed.insert(F);
+      }
+
       if (!SkipInitializes && !A.onlyReadsMemory()) {
         if (inferInitializes(A, *F))
           Changed.insert(F);
diff --git a/llvm/lib/Transforms/Utils/CodeExtractor.cpp b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
index c18cca1b295830..14da9d10ec66cd 100644
--- a/llvm/lib/Transforms/Utils/CodeExtractor.cpp
+++ b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
@@ -1055,6 +1055,7 @@ Function *CodeExtractor::constructFunctionDeclaration(
       case Attribute::ImmArg:
       case Attribute::ByRef:
       case Attribute::WriteOnly:
+      case Attribute::WriteRange:
       case Attribute::Writable:
       case Attribute::DeadOnUnwind:
       case Attribute::Range:
diff --git a/llvm/test/Transforms/GVN/writerange.ll b/llvm/test/Transforms/GVN/writerange.ll
new file mode 100644
index 00000000000000..2d320553278a6c
--- /dev/null
+++ b/llvm/test/Transforms/GVN/writerange.ll
@@ -0,0 +1,34 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -passes='function-attrs,gvn,instcombine' -S < %s | FileCheck %s
+
+%struct.V = type { i64, i64 }
+
+define internal fastcc void @makeSize(ptr noundef %v) noinline {
+; CHECK-LABEL: define internal fastcc void @makeSize(
+; CHECK-SAME: ptr nofree noundef writeonly captures(none) initializes((8, 16)) writerange((8, 16)) [[V:%.*]]) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[CAPACITY:%.*]] = getelementptr inbounds nuw i8, ptr [[V]], i64 8
+; CHECK-NEXT:    store i64 16, ptr [[CAPACITY]], align 8
+; CHECK-NEXT:    ret void
+;
+entry:
+  %capacity = getelementptr inbounds nuw i8, ptr %v, i64 8
+  store i64 16, ptr %capacity, align 8
+  ret void
+}
+
+define i64 @test() {
+; CHECK-LABEL: define i64 @test(
+; CHECK-SAME: ) #[[ATTR1:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    ret i64 1
+;
+entry:
+  %v = alloca %struct.V, align 8
+  store i64 0, ptr %v, align 8
+  call fastcc void @makeSize(ptr noundef %v)
+  %0 = load i64, ptr %v, align 8
+  %cmp = icmp eq i64 %0, 0
+  %conv = select i1 %cmp, i64 1, i64 2
+  ret i64 %conv
+}



More information about the llvm-commits mailing list