Skip to content

Conversation

Icohedron
Copy link
Contributor

@Icohedron Icohedron commented Aug 20, 2025

Fixes #153304

Changes:

  • When writing ConstantExpr GEPs to DXIL bitcode, the bitcode writer will use the old Constant Code CST_CODE_CE_GEP_OLD = 12 instead of the newer CST_CODE_CE_GEP = 32 which is interpreted as an undef in DXIL. Additional context: CST_CODE_CE_GEP = 12 in DXC while the same constant code is labeled CST_CODE_CE_GEP_OLD in LLVM
  • Modifies the PointerTypeAnalysis to be able to analyze pointer-typed constants that appear in the operands of instructions so that the correct type of the ConstantExpr GEP is determined and written into the DXIL bitcode.
  • Adds a PointerTypeAnalysis test and dxil-dis test to ensure that the pointer type of ConstantExpr GEPs are resolved and ConstantExpr GEPs are written to DXIL bitcode correctly

In addition, this PR also adds a missing call to GV.removeDeadConstantUsers() in the DXILFinalizeLinkage pass, and removes an unnecessary manual removal of a ConstantExpr in the DXILFlattenArrays pass.

@llvmbot
Copy link
Member

llvmbot commented Aug 20, 2025

@llvm/pr-subscribers-backend-directx

Author: Deric C. (Icohedron)

Changes

Fixes #153304

Changes:

  • When writing ConstantExpr GEPs to DXIL bitcode, the bitcode writer will use the old Constant Code CST_CODE_CE_GEP_OLD = 12 instead of the newer CST_CODE_CE_GEP = 32 which is interpreted as an undef in DXIL.
  • Modifies the PointerTypeAnalysis to be able to analyze pointer-typed constants that appear in the operands of instructions so that the correct type of the ConstantExpr GEP is determined and written into the DXIL bitcode.
  • Fixes the DXILDataScalarization, DXILFlattenArrays, and DXILLegalize passes so that ConstantExprs are destroyed after replacing its uses. (This issue was causing the PointerTypeAnalysis to incorrectly classify the type of GlobalVariables due to dead ConstantExprs that were supposed to have been removed)
  • Adds a PointerTypeAnalysis test and dxil-dis test to ensure that the pointer type of ConstantExpr GEPs are resolved and ConstantExpr GEPs are written to DXIL bitcode correctly

Full diff: https://github.com/llvm/llvm-project/pull/154446.diff

7 Files Affected:

  • (modified) llvm/lib/Target/DirectX/DXILDataScalarization.cpp (+5-1)
  • (modified) llvm/lib/Target/DirectX/DXILFlattenArrays.cpp (+6)
  • (modified) llvm/lib/Target/DirectX/DXILLegalizePass.cpp (+4)
  • (modified) llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp (+1-1)
  • (modified) llvm/lib/Target/DirectX/DirectXIRPasses/PointerTypeAnalysis.cpp (+22-5)
  • (added) llvm/test/tools/dxil-dis/constantexpr-gep.ll (+26)
  • (modified) llvm/unittests/Target/DirectX/PointerTypeAnalysisTests.cpp (+28)
diff --git a/llvm/lib/Target/DirectX/DXILDataScalarization.cpp b/llvm/lib/Target/DirectX/DXILDataScalarization.cpp
index feecfc0880e25..5eb6382e2177a 100644
--- a/llvm/lib/Target/DirectX/DXILDataScalarization.cpp
+++ b/llvm/lib/Target/DirectX/DXILDataScalarization.cpp
@@ -153,6 +153,8 @@ bool DataScalarizerVisitor::visitLoadInst(LoadInst &LI) {
     NewLoad->setAlignment(LI.getAlign());
     LI.replaceAllUsesWith(NewLoad);
     LI.eraseFromParent();
+    if (CE->use_empty())
+      CE->destroyConstant();
     visitGetElementPtrInst(*OldGEP);
     return true;
   }
@@ -173,6 +175,8 @@ bool DataScalarizerVisitor::visitStoreInst(StoreInst &SI) {
     NewStore->setAlignment(SI.getAlign());
     SI.replaceAllUsesWith(NewStore);
     SI.eraseFromParent();
+    if (CE->use_empty())
+      CE->destroyConstant();
     visitGetElementPtrInst(*OldGEP);
     return true;
   }
@@ -343,7 +347,7 @@ bool DataScalarizerVisitor::visitGetElementPtrInst(GetElementPtrInst &GEPI) {
 
   GOp->replaceAllUsesWith(NewGEP);
 
-  if (auto *CE = dyn_cast<ConstantExpr>(GOp))
+  if (auto *CE = dyn_cast<ConstantExpr>(GOp); CE && CE->use_empty())
     CE->destroyConstant();
   else if (auto *OldGEPI = dyn_cast<GetElementPtrInst>(GOp))
     OldGEPI->eraseFromParent();
diff --git a/llvm/lib/Target/DirectX/DXILFlattenArrays.cpp b/llvm/lib/Target/DirectX/DXILFlattenArrays.cpp
index 7e1436e05a34a..9ab236a9c367d 100644
--- a/llvm/lib/Target/DirectX/DXILFlattenArrays.cpp
+++ b/llvm/lib/Target/DirectX/DXILFlattenArrays.cpp
@@ -166,6 +166,8 @@ bool DXILFlattenArraysVisitor::visitLoadInst(LoadInst &LI) {
       NewLoad->setAlignment(LI.getAlign());
       LI.replaceAllUsesWith(NewLoad);
       LI.eraseFromParent();
+      if (CE->use_empty())
+        CE->destroyConstant();
       visitGetElementPtrInst(*OldGEP);
       return true;
     }
@@ -188,6 +190,8 @@ bool DXILFlattenArraysVisitor::visitStoreInst(StoreInst &SI) {
       NewStore->setAlignment(SI.getAlign());
       SI.replaceAllUsesWith(NewStore);
       SI.eraseFromParent();
+      if (CE->use_empty())
+        CE->destroyConstant();
       visitGetElementPtrInst(*OldGEP);
       return true;
     }
@@ -243,6 +247,8 @@ bool DXILFlattenArraysVisitor::visitGetElementPtrInst(GetElementPtrInst &GEP) {
 
     GEP.replaceAllUsesWith(NewGEPI);
     GEP.eraseFromParent();
+    if (PtrOpGEPCE->use_empty())
+      PtrOpGEPCE->destroyConstant();
     visitGetElementPtrInst(*OldGEPI);
     visitGetElementPtrInst(*NewGEPI);
     return true;
diff --git a/llvm/lib/Target/DirectX/DXILLegalizePass.cpp b/llvm/lib/Target/DirectX/DXILLegalizePass.cpp
index 3427968d199f6..515f64a4d726d 100644
--- a/llvm/lib/Target/DirectX/DXILLegalizePass.cpp
+++ b/llvm/lib/Target/DirectX/DXILLegalizePass.cpp
@@ -143,6 +143,10 @@ static bool fixI8UseChain(Instruction &I,
     LoadInst *NewLoad = Builder.CreateLoad(ElementType, NewGEP);
     ReplacedValues[Load] = NewLoad;
     Load->replaceAllUsesWith(NewLoad);
+    Load->setOperand(Load->getPointerOperandIndex(),
+                     PoisonValue::get(CE->getType()));
+    if (CE->use_empty())
+      CE->destroyConstant();
     ToRemove.push_back(Load);
     return true;
   }
diff --git a/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp b/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp
index 1d79c3018439e..bc1a3a7995bda 100644
--- a/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp
+++ b/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp
@@ -2113,7 +2113,7 @@ void DXILBitcodeWriter::writeConstants(unsigned FirstVal, unsigned LastVal,
         }
         break;
       case Instruction::GetElementPtr: {
-        Code = bitc::CST_CODE_CE_GEP;
+        Code = bitc::CST_CODE_CE_GEP_OLD;
         const auto *GO = cast<GEPOperator>(C);
         if (GO->isInBounds())
           Code = bitc::CST_CODE_CE_INBOUNDS_GEP;
diff --git a/llvm/lib/Target/DirectX/DirectXIRPasses/PointerTypeAnalysis.cpp b/llvm/lib/Target/DirectX/DirectXIRPasses/PointerTypeAnalysis.cpp
index f99bb4f4eaee1..b996075e31ad2 100644
--- a/llvm/lib/Target/DirectX/DirectXIRPasses/PointerTypeAnalysis.cpp
+++ b/llvm/lib/Target/DirectX/DirectXIRPasses/PointerTypeAnalysis.cpp
@@ -15,25 +15,34 @@
 #include "llvm/IR/GlobalVariable.h"
 #include "llvm/IR/Instructions.h"
 #include "llvm/IR/Module.h"
+#include "llvm/IR/Operator.h"
 
 using namespace llvm;
 using namespace llvm::dxil;
 
 namespace {
 
+Type *classifyFunctionType(const Function &F, PointerTypeMap &Map);
+
 // Classifies the type of the value passed in by walking the value's users to
 // find a typed instruction to materialize a type from.
 Type *classifyPointerType(const Value *V, PointerTypeMap &Map) {
   assert(V->getType()->isPointerTy() &&
          "classifyPointerType called with non-pointer");
+
+  // A CallInst will trigger this case, and we want to classify its Function
+  // operand as a Function rather than a generic Value.
+  if (const Function *F = dyn_cast<Function>(V))
+    return classifyFunctionType(*F, Map);
+
   auto It = Map.find(V);
   if (It != Map.end())
     return It->second;
 
   Type *PointeeTy = nullptr;
-  if (auto *Inst = dyn_cast<GetElementPtrInst>(V)) {
-    if (!Inst->getResultElementType()->isPointerTy())
-      PointeeTy = Inst->getResultElementType();
+  if (auto *GEP = dyn_cast<GEPOperator>(V)) {
+    if (!GEP->getResultElementType()->isPointerTy())
+      PointeeTy = GEP->getResultElementType();
   } else if (auto *Inst = dyn_cast<AllocaInst>(V)) {
     PointeeTy = Inst->getAllocatedType();
   } else if (auto *GV = dyn_cast<GlobalVariable>(V)) {
@@ -41,6 +50,11 @@ Type *classifyPointerType(const Value *V, PointerTypeMap &Map) {
   }
 
   for (const auto *User : V->users()) {
+    // This assert is here because there have been ConstantExpr GEPs with no
+    // users causing the pointer type analysis to misclassify the PointeeTy.
+    [[maybe_unused]] const Constant *C = dyn_cast<Constant>(User);
+    assert((!C || !C->use_empty()) && "A Constant should not have no uses");
+
     Type *NewPointeeTy = nullptr;
     if (const auto *Inst = dyn_cast<LoadInst>(User)) {
       NewPointeeTy = Inst->getType();
@@ -49,8 +63,8 @@ Type *classifyPointerType(const Value *V, PointerTypeMap &Map) {
       // When store value is ptr type, cannot get more type info.
       if (NewPointeeTy->isPointerTy())
         continue;
-    } else if (const auto *Inst = dyn_cast<GetElementPtrInst>(User)) {
-      NewPointeeTy = Inst->getSourceElementType();
+    } else if (const auto *GEP = dyn_cast<GEPOperator>(User)) {
+      NewPointeeTy = GEP->getSourceElementType();
     }
     if (NewPointeeTy) {
       // HLSL doesn't support pointers, so it is unlikely to get more than one
@@ -204,6 +218,9 @@ PointerTypeMap PointerTypeAnalysis::run(const Module &M) {
       for (const auto &I : B) {
         if (I.getType()->isPointerTy())
           classifyPointerType(&I, Map);
+        for (const auto &O : I.operands())
+          if (O.get()->getType()->isPointerTy())
+            classifyPointerType(O.get(), Map);
       }
     }
   }
diff --git a/llvm/test/tools/dxil-dis/constantexpr-gep.ll b/llvm/test/tools/dxil-dis/constantexpr-gep.ll
new file mode 100644
index 0000000000000..56d46d071df7a
--- /dev/null
+++ b/llvm/test/tools/dxil-dis/constantexpr-gep.ll
@@ -0,0 +1,26 @@
+; RUN: llc --filetype=obj %s -o - | dxil-dis -o - | FileCheck %s
+target triple = "dxil-unknown-shadermodel6.7-library"
+
+; CHECK: [[GLOBAL:@.*]] = unnamed_addr addrspace(3) global [10 x i32] zeroinitializer, align 4
+@g = local_unnamed_addr addrspace(3) global [10 x i32] zeroinitializer, align 4
+
+define i32 @fn() #0 {
+; CHECK-LABEL:  define i32 @fn()
+; CHECK-NEXT:   [[LOAD:%.*]] = load i32, i32 addrspace(3)* getelementptr inbounds ([10 x i32], [10 x i32] addrspace(3)* [[GLOBAL]], i32 0, i32 1), align 4
+; CHECK-NEXT:   ret i32 [[LOAD]]
+;
+  %gep = getelementptr [10 x i32], ptr addrspace(3) @g, i32 0, i32 1
+  %ld = load i32, ptr addrspace(3) %gep, align 4
+  ret i32 %ld
+}
+
+define i32 @fn2() #0 {
+; CHECK-LABEL:  define i32 @fn2()
+; CHECK-NEXT:   [[LOAD:%.*]] = load i32, i32 addrspace(3)* getelementptr inbounds ([10 x i32], [10 x i32] addrspace(3)* [[GLOBAL]], i32 0, i32 2), align 4
+; CHECK-NEXT:   ret i32 [[LOAD]]
+;
+  %ld = load i32, ptr addrspace(3) getelementptr ([10 x i32], ptr addrspace(3) @g, i32 0, i32 2), align 4
+  ret i32 %ld
+}
+
+attributes #0 = { "hlsl.export" }
diff --git a/llvm/unittests/Target/DirectX/PointerTypeAnalysisTests.cpp b/llvm/unittests/Target/DirectX/PointerTypeAnalysisTests.cpp
index 9d41e94bb0bae..6ae139e076281 100644
--- a/llvm/unittests/Target/DirectX/PointerTypeAnalysisTests.cpp
+++ b/llvm/unittests/Target/DirectX/PointerTypeAnalysisTests.cpp
@@ -8,6 +8,7 @@
 
 #include "DirectXIRPasses/PointerTypeAnalysis.h"
 #include "llvm/AsmParser/Parser.h"
+#include "llvm/IR/Constants.h"
 #include "llvm/IR/Instructions.h"
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/Module.h"
@@ -123,6 +124,33 @@ TEST(PointerTypeAnalysis, DiscoverGEP) {
   EXPECT_THAT(Map, Contains(Pair(IsA<GetElementPtrInst>(), I64Ptr)));
 }
 
+TEST(PointerTypeAnalysis, DiscoverConstantExprGEP) {
+  StringRef Assembly = R"(
+    @g = internal global [10 x i32] zeroinitializer
+    define i32 @test() {
+      %i = load i32, ptr getelementptr ([10 x i32], ptr @g, i64 0, i64 1)
+      ret i32 %i
+    }
+  )";
+
+  LLVMContext Context;
+  SMDiagnostic Error;
+  auto M = parseAssemblyString(Assembly, Error, Context);
+  ASSERT_TRUE(M) << "Bad assembly?";
+
+  PointerTypeMap Map = PointerTypeAnalysis::run(*M);
+  ASSERT_EQ(Map.size(), 3u);
+  Type *I32Ty = Type::getInt32Ty(Context);
+  Type *I32Ptr = TypedPointerType::get(I32Ty, 0);
+  Type *I32ArrPtr = TypedPointerType::get(ArrayType::get(I32Ty, 10), 0);
+  Type *FnTy = FunctionType::get(I32Ty, {}, false);
+
+  EXPECT_THAT(Map, Contains(Pair(IsA<GlobalVariable>(), I32ArrPtr)));
+  EXPECT_THAT(Map,
+              Contains(Pair(IsA<Function>(), TypedPointerType::get(FnTy, 0))));
+  EXPECT_THAT(Map, Contains(Pair(IsA<ConstantExpr>(), I32Ptr)));
+}
+
 TEST(PointerTypeAnalysis, TraceIndirect) {
   StringRef Assembly = R"(
     define i64 @test(ptr %p) {

@@ -173,6 +175,8 @@ bool DataScalarizerVisitor::visitStoreInst(StoreInst &SI) {
NewStore->setAlignment(SI.getAlign());
SI.replaceAllUsesWith(NewStore);
SI.eraseFromParent();
if (CE->use_empty())
Copy link
Member

@farzonl farzonl Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting so even though we erase the StoreInst the CE lives on? Is there maybe a bug in the ConstantExpr tracking? Is there something where we can do this once in a more generic way so we don't have to do that for every ConstExpr when we are deleting the instructions that use constExpr. does it make sense to maybe do something like this on the tail end of eraseFromParent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I spent some time digging some more into it.
Supposedly it is intentional that Constants can linger around even after all its uses are eliminated.
There is a function called removeDeadConstantUsers that should be called before checking users of globals.

/// If there are any dead constant users dangling off of this constant, remove
/// them. This method is useful for clients that want to check to see if a
/// global is unused, but don't want to deal with potentially dead constants
/// hanging off of the globals.
LLVM_ABI void removeDeadConstantUsers() const;

I will reverse the change of all these manual if (CE->use_empty()) CE->destroyConstant(); in each pass, and just call GV->removeDeadConstantUsers() in the PointerTypeAnalysis instead.

@farzonl
Copy link
Member

farzonl commented Aug 21, 2025

Rest of the code looks good. I'm only concered about how we are handling ConstExpr. I feel like the way we are doing it isn't future proofing to the next pass that might operate on them and not know to delete them after instruction erasure.

@Icohedron
Copy link
Contributor Author

Rest of the code looks good. I'm only concered about how we are handling ConstExpr. I feel like the way we are doing it isn't future proofing to the next pass that might operate on them and not know to delete them after instruction erasure.

I changed the handling of ConstantExpr. Each pass no longer has the responsibility of cleaning up dead ConstantExprs.
Now the PointerTypeAnalysis pass will call removeDeadConstantUsers on globals before checking its users.

@Icohedron Icohedron requested review from farzonl and inbelic August 26, 2025 16:04
Copy link
Contributor

@inbelic inbelic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I would like Farzon's approval as well though

if (auto *CE = dyn_cast<ConstantExpr>(GOp))
CE->destroyConstant();
else if (auto *OldGEPI = dyn_cast<GetElementPtrInst>(GOp))
if (auto *OldGEPI = dyn_cast<GetElementPtrInst>(GOp))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little confused what is going on here. In the diff it looks like we always did OldGEPI->eraseFromParent(); was this previously defined somewhere above? Is the diff off? Why do we have to dyn_cast GOp? Shouldn't that already be a GEP?

Copy link
Contributor Author

@Icohedron Icohedron Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOp is indeed a GEPOperator, but it could be a ConstantExpr GEP or an Instruction.
The original implementation here checked if GOp was a ConstantExpr or an Instruction and then called destroyConstant or eraseFromParent respectively.
The change/diff is that destroyConstant is no longer called on GOp if it is a ConstantExpr. But eraseFromParent will still be called if GOp is an Instruction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see this is clearer when you don't combine the +/- diffs

Copy link
Member

@farzonl farzonl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Icohedron Icohedron merged commit 13a6342 into llvm:main Aug 26, 2025
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[DirectX] DXILBitcodeWriter incorrectly writes ConstantExpr GEPs
4 participants