[llvm] [SPIR-V] initial support for @llvm.structured.gep (PR #178668)
Nathan Gauër via llvm-commits
llvm-commits at lists.llvm.org
Mon Feb 9 07:19:10 PST 2026
https://github.com/Keenuts updated https://github.com/llvm/llvm-project/pull/178668
>From 418c6dd5d18b90cd92d7bb6e6467c1722f293928 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nathan=20Gau=C3=ABr?= <brioche at google.com>
Date: Thu, 29 Jan 2026 11:46:43 +0100
Subject: [PATCH 1/2] [SPIR-V] initial support for @llvm.structured.gep
This commit adds initial support to lower the intrinsinc
@llvm.structured.gep into proper SPIR-V.
For now, the backend continues to support both GEP formats.
We might want to revisit this at some point for the logical part.
---
llvm/include/llvm/IR/IntrinsicInst.h | 6 +
llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp | 73 ++++++---
.../test/CodeGen/SPIRV/pointers/sgep-array.ll | 141 ++++++++++++++++++
.../test/CodeGen/SPIRV/pointers/sgep-class.ll | 39 +++++
.../SPIRV/pointers/sgep-dynamic-index.ll | 47 ++++++
.../pointers/sgep-nested-struct-array.ll | 35 +++++
.../SPIRV/pointers/sgep-runtime-array.ll | 32 ++++
.../CodeGen/SPIRV/pointers/sgep-struct.ll | 36 +++++
.../CodeGen/SPIRV/pointers/sgep-vector.ll | 86 +++++++++++
9 files changed, 478 insertions(+), 17 deletions(-)
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-array.ll
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-class.ll
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-dynamic-index.ll
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-nested-struct-array.ll
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-runtime-array.ll
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-struct.ll
create mode 100644 llvm/test/CodeGen/SPIRV/pointers/sgep-vector.ll
diff --git a/llvm/include/llvm/IR/IntrinsicInst.h b/llvm/include/llvm/IR/IntrinsicInst.h
index ba42601085448..784e599c53f44 100644
--- a/llvm/include/llvm/IR/IntrinsicInst.h
+++ b/llvm/include/llvm/IR/IntrinsicInst.h
@@ -1814,6 +1814,12 @@ class StructuredGEPInst : public IntrinsicInst {
return isa<IntrinsicInst>(V) && classof(cast<IntrinsicInst>(V));
}
+ static unsigned getPointerOperandIndex() { return 0; }
+
+ Value *getPointerOperand() const {
+ return getOperand(getPointerOperandIndex());
+ }
+
Type *getBaseType() const {
return getParamAttr(0, Attribute::ElementType).getValueAsType();
}
diff --git a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
index 0ae30a2cdf1ac..a562f86837443 100644
--- a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
@@ -63,6 +63,10 @@ namespace llvm::SPIRV {
namespace {
+static bool isaGEP(const Value *V) {
+ return isa<StructuredGEPInst>(V) || isa<GetElementPtrInst>(V);
+}
+
class SPIRVEmitIntrinsics
: public ModulePass,
public InstVisitor<SPIRVEmitIntrinsics, Instruction *> {
@@ -85,7 +89,7 @@ class SPIRVEmitIntrinsics
DenseMap<Value *, bool> TodoType;
void insertTodoType(Value *Op) {
// TODO: add isa<CallInst>(Op) to no-insert
- if (CanTodoType && !isa<GetElementPtrInst>(Op)) {
+ if (CanTodoType && !isaGEP(Op)) {
auto It = TodoType.try_emplace(Op, true);
if (It.second)
++TodoTypeSz;
@@ -99,7 +103,7 @@ class SPIRVEmitIntrinsics
}
}
bool isTodoType(Value *Op) {
- if (isa<GetElementPtrInst>(Op))
+ if (isaGEP(Op))
return false;
auto It = TodoType.find(Op);
return It != TodoType.end() && It->second;
@@ -250,6 +254,7 @@ class SPIRVEmitIntrinsics
Instruction *visitInstruction(Instruction &I) { return &I; }
Instruction *visitSwitchInst(SwitchInst &I);
Instruction *visitGetElementPtrInst(GetElementPtrInst &I);
+ Instruction *visitIntrinsicInst(IntrinsicInst &I);
Instruction *visitBitCastInst(BitCastInst &I);
Instruction *visitInsertElementInst(InsertElementInst &I);
Instruction *visitExtractElementInst(ExtractElementInst &I);
@@ -513,8 +518,7 @@ void SPIRVEmitIntrinsics::propagateElemType(
Instruction *UI = dyn_cast<Instruction>(U);
// If the instruction was validated already, we need to keep it valid by
// keeping current Op type.
- if (isa<GetElementPtrInst>(UI) ||
- TypeValidated.find(UI) != TypeValidated.end())
+ if (isaGEP(UI) || TypeValidated.find(UI) != TypeValidated.end())
replaceUsesOfWithSpvPtrcast(Op, ElemTy, UI, Ptrcasts);
}
}
@@ -544,8 +548,7 @@ void SPIRVEmitIntrinsics::propagateElemTypeRec(
Instruction *UI = dyn_cast<Instruction>(U);
// If the instruction was validated already, we need to keep it valid by
// keeping current Op type.
- if (isa<GetElementPtrInst>(UI) ||
- TypeValidated.find(UI) != TypeValidated.end())
+ if (isaGEP(UI) || TypeValidated.find(UI) != TypeValidated.end())
replaceUsesOfWithSpvPtrcast(Op, CastElemTy, UI, Ptrcasts);
}
}
@@ -785,6 +788,8 @@ Type *SPIRVEmitIntrinsics::deduceElementTypeHelper(
maybeAssignPtrType(Ty, I, Ref->getAllocatedType(), UnknownElemTypeI8);
} else if (auto *Ref = dyn_cast<GetElementPtrInst>(I)) {
Ty = getGEPType(Ref);
+ } else if (auto *SGEP = dyn_cast<StructuredGEPInst>(I)) {
+ Ty = SGEP->getResultElementType();
} else if (auto *Ref = dyn_cast<LoadInst>(I)) {
Value *Op = Ref->getPointerOperand();
Type *KnownTy = GR->findDeducedElementType(Op);
@@ -1223,6 +1228,12 @@ void SPIRVEmitIntrinsics::deduceOperandElementType(
KnownElemTy = Ref->getSourceElementType();
Ops.push_back(std::make_pair(Ref->getPointerOperand(),
GetElementPtrInst::getPointerOperandIndex()));
+ } else if (auto *Ref = dyn_cast<StructuredGEPInst>(I)) {
+ if (GR->findDeducedElementType(Ref->getPointerOperand()))
+ return;
+ KnownElemTy = Ref->getBaseType();
+ Ops.push_back(std::make_pair(Ref->getPointerOperand(),
+ StructuredGEPInst::getPointerOperandIndex()));
} else if (auto *Ref = dyn_cast<LoadInst>(I)) {
KnownElemTy = I->getType();
if (isUntypedPointerTy(KnownElemTy))
@@ -1619,6 +1630,26 @@ static bool isFirstIndexZero(const GetElementPtrInst *GEP) {
return false;
}
+Instruction *SPIRVEmitIntrinsics::visitIntrinsicInst(IntrinsicInst &I) {
+ auto *SGEP = dyn_cast<StructuredGEPInst>(&I);
+ if (!SGEP)
+ return &I;
+
+ IRBuilder<> B(I.getParent());
+ B.SetInsertPoint(&I);
+ SmallVector<Type *, 2> Types = {I.getType(), I.getOperand(0)->getType()};
+ SmallVector<Value *, 4> Args;
+ Args.push_back(/* inBounds= */ B.getInt1(true));
+ Args.push_back(I.getOperand(0));
+ Args.push_back(/* zero index */ B.getInt32(0));
+ for (unsigned I = 0; I < SGEP->getNumIndices(); ++I)
+ Args.push_back(SGEP->getIndexOperand(I));
+
+ auto *NewI = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args});
+ replaceAllUsesWithAndErase(B, &I, NewI);
+ return NewI;
+}
+
Instruction *SPIRVEmitIntrinsics::visitGetElementPtrInst(GetElementPtrInst &I) {
IRBuilder<> B(I.getParent());
B.SetInsertPoint(&I);
@@ -1787,7 +1818,7 @@ void SPIRVEmitIntrinsics::replacePointerOperandWithPtrCast(
return;
} else if (isTodoType(Pointer)) {
eraseTodoType(Pointer);
- if (!isa<CallInst>(Pointer) && !isa<GetElementPtrInst>(Pointer)) {
+ if (!isa<CallInst>(Pointer) && !isaGEP(Pointer)) {
// If this wouldn't be the first spv_ptrcast but existing type info is
// uncomplete, update spv_assign_ptr_type arguments.
if (CallInst *AssignCI = GR->findAssignPtrTypeInstr(Pointer)) {
@@ -2826,7 +2857,7 @@ void SPIRVEmitIntrinsics::applyDemangledPtrArgTypes(IRBuilder<> &B) {
B.SetCurrentDebugLocation(DebugLoc());
GR->buildAssignPtr(B, ElemTy, Arg);
}
- } else if (isa<GetElementPtrInst>(Param)) {
+ } else if (isaGEP(Param)) {
replaceUsesOfWithSpvPtrcast(Param, normalizeType(ElemTy), CI,
Ptrcasts);
} else if (isa<Instruction>(Param)) {
@@ -2899,18 +2930,26 @@ bool SPIRVEmitIntrinsics::runOnFunction(Function &Func) {
// Data structure for dead instructions that were simplified and replaced.
SmallPtrSet<Instruction *, 4> DeadInsts;
for (auto &I : instructions(Func)) {
- auto *Ref = dyn_cast<GetElementPtrInst>(&I);
- if (!Ref || GR->findDeducedElementType(Ref))
+ auto *GEP = dyn_cast<GetElementPtrInst>(&I);
+ auto *SGEP = dyn_cast<StructuredGEPInst>(&I);
+
+ if ((!GEP && !SGEP) || GR->findDeducedElementType(&I))
continue;
- GetElementPtrInst *NewGEP = simplifyZeroLengthArrayGepInst(Ref);
+ if (SGEP) {
+ GR->addDeducedElementType(SGEP,
+ normalizeType(SGEP->getResultElementType()));
+ continue;
+ }
+
+ GetElementPtrInst *NewGEP = simplifyZeroLengthArrayGepInst(GEP);
if (NewGEP) {
- Ref->replaceAllUsesWith(NewGEP);
- DeadInsts.insert(Ref);
- Ref = NewGEP;
+ GEP->replaceAllUsesWith(NewGEP);
+ DeadInsts.insert(GEP);
+ GEP = NewGEP;
}
- if (Type *GepTy = getGEPType(Ref))
- GR->addDeducedElementType(Ref, normalizeType(GepTy));
+ if (Type *GepTy = getGEPType(GEP))
+ GR->addDeducedElementType(GEP, normalizeType(GepTy));
}
// Remove dead instructions that were simplified and replaced.
for (auto *I : DeadInsts) {
@@ -3006,7 +3045,7 @@ bool SPIRVEmitIntrinsics::postprocessTypes(Module &M) {
DenseMap<Value *, SmallPtrSet<Value *, 4>> ToProcess;
for (auto [Op, Enabled] : TodoType) {
// TODO: add isa<CallInst>(Op) to continue
- if (!Enabled || isa<GetElementPtrInst>(Op))
+ if (!Enabled || isaGEP(Op))
continue;
CallInst *AssignCI = GR->findAssignPtrTypeInstr(Op);
Type *KnownTy = GR->findDeducedElementType(Op);
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-array.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-array.ll
new file mode 100644
index 0000000000000..1582e2404d3fa
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-array.ll
@@ -0,0 +1,141 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+%struct.S = type { i32, i32 }
+%struct.S2 = type { i32, %struct.S, i32 }
+
+; CHECK-DAG: %[[#uint:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#ulong:]] = OpTypeInt 64 0
+; CHECK-DAG: %[[#ulong_0:]] = OpConstant %[[#ulong]] 0
+; CHECK-DAG: %[[#ulong_1:]] = OpConstant %[[#ulong]] 1
+; CHECK-DAG: %[[#ulong_2:]] = OpConstant %[[#ulong]] 2
+; CHECK-DAG: %[[#ulong_3:]] = OpConstant %[[#ulong]] 3
+; CHECK-DAG: %[[#uint_0:]] = OpConstant %[[#uint]] 0
+; CHECK-DAG: %[[#uint_1:]] = OpConstant %[[#uint]] 1
+; CHECK-DAG: %[[#uint_5:]] = OpConstant %[[#uint]] 5
+; CHECK-DAG: %[[#ptr_uint:]] = OpTypePointer Function %[[#uint]]
+; CHECK-DAG: %[[#arr_uint_5:]] = OpTypeArray %[[#uint]] %[[#uint_5]]
+; CHECK-DAG: %[[#ptr_arr_uint_5:]] = OpTypePointer Function %[[#arr_uint_5]]
+; CHECK-DAG: %[[#S:]] = OpTypeStruct %[[#uint]] %[[#uint]]
+; CHECK-DAG: %[[#ptr_S:]] = OpTypePointer Function %[[#S]]
+; CHECK-DAG: %[[#arr_S_5:]] = OpTypeArray %[[#S]] %[[#uint_5]]
+; CHECK-DAG: %[[#ptr_arr_S_5:]] = OpTypePointer Function %[[#arr_S_5]]
+; CHECK-DAG: %[[#S2:]] = OpTypeStruct %[[#uint]] %[[#S]] %[[#uint]]
+; CHECK-DAG: %[[#ptr_S2:]] = OpTypePointer Function %[[#S2]]
+; CHECK-DAG: %[[#arr_S2_5:]] = OpTypeArray %[[#S2]] %[[#uint_5]]
+; CHECK-DAG: %[[#ptr_arr_S2_5:]] = OpTypePointer Function %[[#arr_S2_5]]
+
+define spir_func void @array_load_store(ptr %a) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ %tmp = alloca [5 x i32], align 4
+; CHECK: %[[#tmp:]] = OpVariable %[[#ptr_arr_uint_5]] Function
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([3 x i32]) %a, i64 2)
+ %2 = load i32, ptr %1, align 4
+; CHECK: %[[#A:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#]] %[[#ulong_2]]
+; CHECK: %[[#B:]] = OpLoad %[[#uint]] %[[#A]] Aligned 4
+
+ %3 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([5 x i32]) %tmp, i64 3)
+ store i32 %2, ptr %3, align 4
+; CHECK: %[[#C:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#tmp]] %[[#ulong_3]]
+; CHECK: OpStore %[[#C]] %[[#B]] Aligned 4
+
+ ret void
+}
+
+define spir_func void @array_struct_load_store(ptr %a) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ %tmp = alloca [5 x %struct.S], align 4
+; CHECK: %[[#tmp:]] = OpVariable %[[#ptr_arr_S_5]] Function
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([3 x %struct.S]) %a, i64 2)
+ %2 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.S) %1, i64 1)
+ %3 = load i32, ptr %2, align 4
+; CHECK: %[[#A:]] = OpInBoundsAccessChain %[[#ptr_S]] %[[#]] %[[#ulong_2]]
+; CHECK: %[[#B:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#A]] %[[#ulong_1]]
+; CHECK: %[[#C:]] = OpLoad %[[#uint]] %[[#B]] Aligned 4
+
+ %4 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([5 x %struct.S]) %tmp, i64 3)
+ %5 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.S) %4, i32 0)
+ store i32 %3, ptr %5, align 4
+; CHECK: %[[#D:]] = OpInBoundsAccessChain %[[#ptr_S]] %[[#tmp]] %[[#ulong_3]]
+; CHECK: %[[#E:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#D]] %[[#uint_0]]
+; CHECK: OpStore %[[#E]] %[[#C]] Aligned 4
+
+ ret void
+}
+
+define spir_func void @array_struct_load_store_combined(ptr %a) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ %tmp = alloca [5 x %struct.S], align 4
+; CHECK: %[[#tmp:]] = OpVariable %[[#ptr_arr_S_5]] Function
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([3 x %struct.S]) %a, i64 2, i64 1)
+ %2 = load i32, ptr %1, align 4
+; CHECK: %[[#A:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#]] %[[#ulong_2]] %[[#ulong_1]]
+; CHECK: %[[#B:]] = OpLoad %[[#uint]] %[[#A]] Aligned 4
+
+ %3 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([5 x %struct.S]) %tmp, i64 3, i32 0)
+ store i32 %2, ptr %3, align 4
+; CHECK: %[[#C:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#tmp]] %[[#ulong_3]] %[[#uint_0]]
+; CHECK: OpStore %[[#C]] %[[#B]] Aligned 4
+
+ ret void
+}
+
+define spir_func void @array_nested_struct_load_store(ptr %a) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ %tmp = alloca [5 x %struct.S2], align 4
+; CHECK: %[[#tmp:]] = OpVariable %[[#ptr_arr_S2_5]] Function
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([3 x %struct.S2]) %a, i64 1)
+ %2 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.S2) %1, i64 1)
+ %3 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.S) %2, i64 0)
+ %4 = load i32, ptr %3, align 4
+; CHECK: %[[#A:]] = OpInBoundsAccessChain %[[#ptr_S2]] %[[#]] %[[#ulong_1]]
+; CHECK: %[[#B:]] = OpInBoundsAccessChain %[[#ptr_S]] %[[#A]] %[[#ulong_1]]
+; CHECK: %[[#C:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#B]] %[[#ulong_0]]
+; CHECK: %[[#D:]] = OpLoad %[[#uint]] %[[#C]] Aligned 4
+
+ %5 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([5 x %struct.S2]) %tmp, i64 3)
+ %6 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.S2) %5, i32 1)
+ %7 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.S) %6, i32 0)
+ store i32 %4, ptr %7, align 4
+; CHECK: %[[#E:]] = OpInBoundsAccessChain %[[#ptr_S2]] %[[#tmp]] %[[#ulong_3]]
+; CHECK: %[[#F:]] = OpInBoundsAccessChain %[[#ptr_S]] %[[#E]] %[[#uint_1]]
+; CHECK: %[[#G:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#F]] %[[#uint_0]]
+; CHECK: OpStore %[[#G]] %[[#D]] Aligned 4
+
+ ret void
+}
+
+define spir_func void @array_nested_struct_load_store_combined(ptr %a) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ %tmp = alloca [5 x %struct.S2], align 4
+; CHECK: %[[#tmp:]] = OpVariable %[[#ptr_arr_S2_5]] Function
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([3 x %struct.S2]) %a, i64 1, i64 1, i64 0)
+ %2 = load i32, ptr %1, align 4
+; CHECK: %[[#A:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#]] %[[#ulong_1]] %[[#ulong_1]] %[[#ulong_0]]
+; CHECK: %[[#B:]] = OpLoad %[[#uint]] %[[#A]] Aligned 4
+
+ %3 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([5 x %struct.S2]) %tmp, i64 3, i32 1, i32 0)
+ store i32 %2, ptr %3, align 4
+; CHECK: %[[#C:]] = OpInBoundsAccessChain %[[#ptr_uint]] %[[#tmp]] %[[#ulong_3]] %[[#uint_1]] %[[#uint_0]]
+; CHECK: OpStore %[[#C]] %[[#B]] Aligned 4
+
+ ret void
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+
+declare ptr @llvm.structured.gep.p0(ptr, ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #2 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-class.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-class.ll
new file mode 100644
index 0000000000000..6f85bbd2e6bad
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-class.ll
@@ -0,0 +1,39 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+%class.Base = type { i32 }
+%class.Derived = type { %class.Base, float }
+
+; CHECK-DAG: %[[#int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#Base:]] = OpTypeStruct %[[#int]]
+; CHECK-DAG: %[[#Derived:]] = OpTypeStruct %[[#Base]] %[[#float]]
+; CHECK-DAG: %[[#ptr_Derived:]] = OpTypePointer Function %[[#Derived]]
+; CHECK-DAG: %[[#ptr_Base:]] = OpTypePointer Function %[[#Base]]
+; CHECK-DAG: %[[#ptr_int:]] = OpTypePointer Function %[[#int]]
+; CHECK-DAG: %[[#idx_0:]] = OpConstant %[[#int]] 0
+
+define spir_func void @class_access(ptr %d) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#d_var:]] = OpFunctionParameter %[[#ptr_Derived]]
+
+ ; Access Base part
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%class.Derived) %d, i32 0)
+ ; CHECK: %[[#ptr_base:]] = OpInBoundsAccessChain %[[#ptr_Base]] %[[#d_var]] %[[#idx_0]]
+
+ ; Access field in Base
+ %2 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%class.Base) %1, i32 0)
+ ; CHECK: %[[#ptr_field:]] = OpInBoundsAccessChain %[[#ptr_int]] %[[#ptr_base]] %[[#idx_0]]
+
+ store i32 42, ptr %2, align 4
+ ; CHECK: OpStore %[[#ptr_field]]
+
+ ret void
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+declare ptr @llvm.structured.gep.p0(ptr, ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-dynamic-index.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-dynamic-index.ll
new file mode 100644
index 0000000000000..5df35e8c86738
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-dynamic-index.ll
@@ -0,0 +1,47 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+; CHECK-DAG: %[[#int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#long:]] = OpTypeInt 64 0
+; CHECK-DAG: %[[#ptr_int:]] = OpTypePointer Function %[[#int]]
+; CHECK-DAG: %[[#idx_10:]] = OpConstant %[[#int]] 10
+; CHECK-DAG: %[[#array:]] = OpTypeArray %[[#int]] %[[#idx_10]]
+; CHECK-DAG: %[[#ptr_array:]] = OpTypePointer Function %[[#array]]
+
+define spir_func i32 @dynamic_access(ptr %arr, i32 %idx) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#arr_var:]] = OpFunctionParameter %[[#ptr_array]]
+ ; CHECK: %[[#idx_var:]] = OpFunctionParameter %[[#int]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([10 x i32]) %arr, i32 %idx)
+ ; CHECK: %[[#ptr_elem:]] = OpInBoundsAccessChain %[[#ptr_int]] %[[#arr_var]] %[[#idx_var]]
+
+ %2 = load i32, ptr %1, align 4
+ ; CHECK: %[[#val:]] = OpLoad %[[#int]] %[[#ptr_elem]] Aligned 4
+
+ ret i32 %2
+ ; CHECK: OpReturnValue %[[#val]]
+}
+
+define spir_func i32 @dynamic_access_i64(ptr %arr, i64 %idx) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#arr_var2:]] = OpFunctionParameter %[[#ptr_array]]
+ ; CHECK: %[[#idx_var2:]] = OpFunctionParameter %[[#long]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([10 x i32]) %arr, i64 %idx)
+ ; CHECK: %[[#ptr_elem2:]] = OpInBoundsAccessChain %[[#ptr_int]] %[[#arr_var2]] %[[#idx_var2]]
+
+ %2 = load i32, ptr %1, align 4
+ ; CHECK: %[[#val2:]] = OpLoad %[[#int]] %[[#ptr_elem2]] Aligned 4
+
+ ret i32 %2
+ ; CHECK: OpReturnValue %[[#val2]]
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+declare ptr @llvm.structured.gep.p0(ptr, ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-nested-struct-array.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-nested-struct-array.ll
new file mode 100644
index 0000000000000..785fdd8fac242
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-nested-struct-array.ll
@@ -0,0 +1,35 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+%struct.Inner = type { i32, <4 x float> }
+%struct.Outer = type { [5 x %struct.Inner], i32 }
+
+; CHECK-DAG: %[[#float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#vec4:]] = OpTypeVector %[[#float]] 4
+; CHECK-DAG: %[[#int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#Inner:]] = OpTypeStruct %[[#int]] %[[#vec4]]
+; CHECK-DAG: %[[#idx_5:]] = OpConstant %[[#int]] 5
+; CHECK-DAG: %[[#Array:]] = OpTypeArray %[[#Inner]] %[[#idx_5]]
+; CHECK-DAG: %[[#Outer:]] = OpTypeStruct %[[#Array]] %[[#int]]
+; CHECK-DAG: %[[#ptr_Outer:]] = OpTypePointer Function %[[#Outer]]
+; CHECK-DAG: %[[#ptr_float:]] = OpTypePointer Function %[[#float]]
+
+define spir_func float @nested_access(ptr %obj) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#obj_var:]] = OpFunctionParameter %[[#ptr_Outer]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.Outer) %obj, i32 0, i32 2, i32 1, i32 1)
+ ; CHECK: %[[#ptr_elem:]] = OpInBoundsAccessChain %[[#ptr_float]] %[[#obj_var]] %[[#]] %[[#]] %[[#]] %[[#]]
+
+ %2 = load float, ptr %1, align 4
+ ; CHECK: %[[#val:]] = OpLoad %[[#float]] %[[#ptr_elem]] Aligned 4
+
+ ret float %2
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+declare ptr @llvm.structured.gep.p0(ptr, ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-runtime-array.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-runtime-array.ll
new file mode 100644
index 0000000000000..c7597598d415f
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-runtime-array.ll
@@ -0,0 +1,32 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+%struct.RuntimeArray = type { [0 x i32] }
+
+ at buffer = external addrspace(11) global %struct.RuntimeArray
+
+; CHECK-DAG: %[[#int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#ptr_sb_int:]] = OpTypePointer StorageBuffer %[[#int]]
+; CHECK-DAG: %[[#rt_array:]] = OpTypeRuntimeArray %[[#int]]
+; CHECK-DAG: %[[#struct:]] = OpTypeStruct %[[#rt_array]]
+; CHECK-DAG: %[[#ptr_sb_struct:]] = OpTypePointer StorageBuffer %[[#struct]]
+; CHECK-DAG: %[[#buffer:]] = OpVariable %[[#ptr_sb_struct]] StorageBuffer
+
+define spir_func i32 @runtime_array_access(i32 %idx) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+
+ %1 = call ptr addrspace(11) (ptr addrspace(11), ...) @llvm.structured.gep.p11(ptr addrspace(11) elementtype(%struct.RuntimeArray) @buffer, i32 0, i32 %idx)
+ ; CHECK: %[[#ptr_elem:]] = OpInBoundsAccessChain %[[#ptr_sb_int]] %[[#buffer]] %[[#]] %[[#]]
+
+ %2 = load i32, ptr addrspace(11) %1, align 4
+ ; CHECK: %[[#val:]] = OpLoad %[[#int]] %[[#ptr_elem]] Aligned 4
+
+ ret i32 %2
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+declare ptr addrspace(11) @llvm.structured.gep.p11(ptr addrspace(11), ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-struct.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-struct.ll
new file mode 100644
index 0000000000000..948564fbbc6df
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-struct.ll
@@ -0,0 +1,36 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+%struct.Simple = type { i32, float, i64 }
+
+; CHECK-DAG: %[[#int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#long:]] = OpTypeInt 64 0
+; CHECK-DAG: %[[#struct:]] = OpTypeStruct %[[#int]] %[[#float]] %[[#long]]
+; CHECK-DAG: %[[#ptr_struct:]] = OpTypePointer Function %[[#struct]]
+; CHECK-DAG: %[[#ptr_float:]] = OpTypePointer Function %[[#float]]
+; CHECK-DAG: %[[#idx_1:]] = OpConstant %[[#int]] 1
+
+define spir_func void @struct_access(ptr %s, ptr %out) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#s_var:]] = OpFunctionParameter %[[#ptr_struct]]
+ ; CHECK: %[[#out_var:]] = OpFunctionParameter %[[#ptr_float]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(%struct.Simple) %s, i32 1)
+ ; CHECK: %[[#ptr_field:]] = OpInBoundsAccessChain %[[#ptr_float]] %[[#s_var]] %[[#idx_1]]
+
+ %2 = load float, ptr %1, align 4
+ ; CHECK: %[[#val:]] = OpLoad %[[#float]] %[[#ptr_field]] Aligned 4
+
+ store float %2, ptr %out, align 4
+ ; CHECK: OpStore %[[#out_var]] %[[#val]] Aligned 4
+
+ ret void
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+declare ptr @llvm.structured.gep.p0(ptr, ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/pointers/sgep-vector.ll b/llvm/test/CodeGen/SPIRV/pointers/sgep-vector.ll
new file mode 100644
index 0000000000000..8399cb8f31cef
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/sgep-vector.ll
@@ -0,0 +1,86 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-pc-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}
+
+; CHECK-DAG: %[[#float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#vec4:]] = OpTypeVector %[[#float]] 4
+; CHECK-DAG: %[[#ptr_vec4:]] = OpTypePointer Function %[[#vec4]]
+; CHECK-DAG: %[[#ptr_float:]] = OpTypePointer Function %[[#float]]
+; CHECK-DAG: %[[#int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#idx_0:]] = OpConstant %[[#int]] 0
+; CHECK-DAG: %[[#idx_1:]] = OpConstant %[[#int]] 1
+; CHECK-DAG: %[[#idx_2:]] = OpConstant %[[#int]] 2
+; CHECK-DAG: %[[#idx_5:]] = OpConstant %[[#int]] 5
+; CHECK-DAG: %[[#idx_10:]] = OpConstant %[[#int]] 10
+; CHECK-DAG: %[[#array_vec4:]] = OpTypeArray %[[#vec4]] %[[#idx_2]]
+; CHECK-DAG: %[[#ptr_array_vec4:]] = OpTypePointer Function %[[#array_vec4]]
+; CHECK-DAG: %[[#array_float:]] = OpTypeArray %[[#float]] %[[#idx_10]]
+; CHECK-DAG: %[[#ptr_array_float:]] = OpTypePointer Function %[[#array_float]]
+
+define spir_func void @vector_access(ptr %vec, ptr %out) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#vec_var:]] = OpFunctionParameter %[[#ptr_vec4]]
+ ; CHECK: %[[#out_var:]] = OpFunctionParameter %[[#ptr_float]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype(<4 x float>) %vec, i32 2)
+ ; CHECK: %[[#ptr_elem:]] = OpInBoundsAccessChain %[[#ptr_float]] %[[#vec_var]] %[[#idx_2]]
+
+ %2 = load float, ptr %1, align 4
+ ; CHECK: %[[#val:]] = OpLoad %[[#float]] %[[#ptr_elem]] Aligned 4
+
+ store float %2, ptr %out, align 4
+ ; CHECK: OpStore %[[#out_var]] %[[#val]] Aligned 4
+
+ ret void
+}
+
+define spir_func void @array_of_vectors_access(ptr %arr, ptr %out) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#arr_var:]] = OpFunctionParameter %[[#ptr_array_vec4]]
+ ; CHECK: %[[#out_var2:]] = OpFunctionParameter %[[#ptr_float]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([2 x <4 x float>]) %arr, i32 1, i32 2)
+ ; CHECK: %[[#ptr_elem2:]] = OpInBoundsAccessChain %[[#ptr_float]] %[[#arr_var]] %[[#idx_1]] %[[#idx_2]]
+
+ %2 = load float, ptr %1, align 4
+ ; CHECK: %[[#val2:]] = OpLoad %[[#float]] %[[#ptr_elem2]] Aligned 4
+
+ store float %2, ptr %out, align 4
+ ; CHECK: OpStore %[[#out_var2]] %[[#val2]] Aligned 4
+
+ ret void
+}
+
+define spir_func void @array_access(ptr %arr, ptr %out) convergent {
+entry:
+ %0 = call token @llvm.experimental.convergence.entry()
+ ; CHECK: %[[#arr_var3:]] = OpFunctionParameter %[[#ptr_array_float]]
+ ; CHECK: %[[#out_var3:]] = OpFunctionParameter %[[#ptr_float]]
+
+ %1 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([10 x float]) %arr, i32 0)
+ ; CHECK: %[[#ptr_elem3_0:]] = OpInBoundsAccessChain %[[#ptr_float]] %[[#arr_var3]] %[[#idx_0]]
+
+ %2 = load float, ptr %1, align 4
+ ; CHECK: %[[#val3_0:]] = OpLoad %[[#float]] %[[#ptr_elem3_0]] Aligned 4
+
+ %3 = call ptr (ptr, ...) @llvm.structured.gep.p0(ptr elementtype([10 x float]) %arr, i32 5)
+ ; CHECK: %[[#ptr_elem3_5:]] = OpInBoundsAccessChain %[[#ptr_float]] %[[#arr_var3]] %[[#idx_5]]
+
+ %4 = load float, ptr %3, align 4
+ ; CHECK: %[[#val3_5:]] = OpLoad %[[#float]] %[[#ptr_elem3_5]] Aligned 4
+
+ %5 = fadd float %2, %4
+ ; CHECK: %[[#res:]] = OpFAdd %[[#float]] %[[#val3_0]] %[[#val3_5]]
+
+ store float %5, ptr %out, align 4
+ ; CHECK: OpStore %[[#out_var3]] %[[#res]] Aligned 4
+
+ ret void
+}
+
+declare token @llvm.experimental.convergence.entry() #1
+declare ptr @llvm.structured.gep.p0(ptr, ...) #3
+
+attributes #1 = { convergent nocallback nofree nosync nounwind willreturn memory(none) }
+attributes #3 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
>From f71af31cb6e8ac526912e8814aa6c11ddda2f216 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nathan=20Gau=C3=ABr?= <brioche at google.com>
Date: Thu, 5 Feb 2026 14:42:33 +0100
Subject: [PATCH 2/2] pr-feedback
---
llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
index a562f86837443..f12321ebc63fe 100644
--- a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
@@ -1642,10 +1642,10 @@ Instruction *SPIRVEmitIntrinsics::visitIntrinsicInst(IntrinsicInst &I) {
Args.push_back(/* inBounds= */ B.getInt1(true));
Args.push_back(I.getOperand(0));
Args.push_back(/* zero index */ B.getInt32(0));
- for (unsigned I = 0; I < SGEP->getNumIndices(); ++I)
- Args.push_back(SGEP->getIndexOperand(I));
+ for (unsigned J = 0; J < SGEP->getNumIndices(); ++J)
+ Args.push_back(SGEP->getIndexOperand(J));
- auto *NewI = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args});
+ auto *NewI = B.CreateIntrinsic(Intrinsic::spv_gep, Types, Args);
replaceAllUsesWithAndErase(B, &I, NewI);
return NewI;
}
More information about the llvm-commits
mailing list