[llvm] [SPIR-V] Bitcast pointers extracted from or inserted into composites (PR #227599)
Tim Besard via llvm-commits
llvm-commits at lists.llvm.org
Wed Sep 30 01:12:00 PDT 2026
https://github.com/maleadt created https://github.com/llvm/llvm-project/pull/227599
When a pointer is pulled out of an aggregate with `extractvalue`, the SPIR-V back-end deduces its pointee type from how it's used afterwards. The aggregate member, however, has a type of its own: a `ptr` field in a struct is lowered as a pointer to `i8`. If the two disagree, we emit an `OpCompositeExtract` whose result type doesn't match the member, which is invalid SPIR-V:
```llvm
define internal { ptr, i8 } @f() {
ret { ptr, i8 } { ptr null, i8 1 }
}
define spir_kernel void @k(ptr addrspace(1) %out) {
%r = call { ptr, i8 } @f()
%p = extractvalue { ptr, i8 } %r, 0
%v = load i32, ptr %p
store i32 %v, ptr addrspace(1) %out
ret void
}
```
```
$ llc -mtriple=spirv64-unknown-unknown -filetype=obj test.ll -o - | spirv-val -
error: line 28: Result type (OpTypePointer) does not match the type that results from indexing into the composite (OpTypePointer).
%17 = OpCompositeExtract %_ptr_Function_uint %16 0
```
`insertvalue` has the same problem the other way around: inserting a pointer to `i32` (e.g. an `alloca i32`) into that `{ ptr, i8 }` produces an `OpCompositeInsert` whose object type doesn't match the member.
This PR fixes both in the instruction selector. When the pointer type we want doesn't match the member's type, `OpCompositeExtract` now extracts the member with its own type and follows it with an `OpBitcast` to the deduced type. `OpCompositeInsert` bitcasts the pointer to the member type before inserting it. The example above now lowers to:
```
%17 = OpCompositeExtract %_ptr_Function_uchar %16 0
%18 = OpBitcast %_ptr_Function_uint %17
%19 = OpLoad %uint %18 Aligned 4
```
The selector already handles `OpPtrEqual` operands the same way. Doing it here, rather than in `SPIRVEmitIntrinsics`, means we compare the actual SPIR-V types, including composites whose pointer members got a more specific type (as for constant aggregates).
Julia produces this pattern for union-typed values: a function that returns a bits union (like `Union{Missing, Int32}`) and isn't inlined hands back a `{ ptr, i8 }` pair, and the caller loads through the extracted pointer. That's how we ran into this, in JuliaGPU/OpenCL.jl#517. With this patch, the kernel from that issue passes `spirv-val` and gives correct results on PoCL.
>From 14b0922f071075352c9a856e990121592c14edcb Mon Sep 17 00:00:00 2001
From: Tim Besard <tim.besard at gmail.com>
Date: Wed, 30 Sep 2026 10:10:40 +0200
Subject: [PATCH] [SPIR-V] Bitcast pointers extracted from or inserted into
composites
The pointee type of a pointer is deduced from its uses, so a pointer that
is extracted from an aggregate can be typed differently from the aggregate
member it came from (e.g. `load i32` from the pointer member of a
`{ ptr, i8 }`, which is lowered as a pointer to i8). OpCompositeExtract and
OpCompositeInsert require the object type to match the member type
exactly, so this produced invalid SPIR-V.
Extract the member with its own type and bitcast it to the deduced type,
and conversely bitcast an inserted pointer to the member type.
---
.../Target/SPIRV/SPIRVInstructionSelector.cpp | 77 ++++++++++++++++++-
.../composite-ptr-member-bitcast.ll | 67 ++++++++++++++++
2 files changed, 140 insertions(+), 4 deletions(-)
create mode 100644 llvm/test/CodeGen/SPIRV/instructions/composite-ptr-member-bitcast.ll
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index 143e1c0e3ef2a..aba277e96cce9 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -358,6 +358,12 @@ class SPIRVInstructionSelector : public InstructionSelector {
MachineInstr &I) const;
bool selectInsertVal(Register ResVReg, SPIRVTypeInst ResType,
MachineInstr &I) const;
+ // Returns the type of the member of \p CompositeType selected by the
+ // constant indices in the operands of \p I starting at \p FirstIdxOp, or a
+ // null type if it cannot be determined.
+ SPIRVTypeInst getCompositeMemberType(SPIRVTypeInst CompositeType,
+ const MachineInstr &I,
+ unsigned FirstIdxOp) const;
bool selectExtractElt(Register ResVReg, SPIRVTypeInst ResType,
MachineInstr &I) const;
bool selectInsertElt(Register ResVReg, SPIRVTypeInst ResType,
@@ -4971,15 +4977,58 @@ bool SPIRVInstructionSelector::selectOpUndef(Register ResVReg,
return true;
}
+SPIRVTypeInst
+SPIRVInstructionSelector::getCompositeMemberType(SPIRVTypeInst CompositeType,
+ const MachineInstr &I,
+ unsigned FirstIdxOp) const {
+ SPIRVTypeInst Ty = CompositeType;
+ for (unsigned i = FirstIdxOp; Ty && i < I.getNumOperands(); i++) {
+ switch (Ty->getOpcode()) {
+ case SPIRV::OpTypeStruct: {
+ uint64_t Idx = foldImm(I.getOperand(i), MRI);
+ if (Idx + 1 >= Ty->getNumOperands())
+ return nullptr;
+ Ty = GR.getSPIRVTypeForVReg(Ty->getOperand(Idx + 1).getReg());
+ break;
+ }
+ case SPIRV::OpTypeArray:
+ case SPIRV::OpTypeVector:
+ Ty = GR.getSPIRVTypeForVReg(Ty->getOperand(1).getReg());
+ break;
+ default:
+ return nullptr;
+ }
+ }
+ return Ty;
+}
+
bool SPIRVInstructionSelector::selectInsertVal(Register ResVReg,
SPIRVTypeInst ResType,
MachineInstr &I) const {
MachineBasicBlock &BB = *I.getParent();
+ // The pointee type of a pointer is deduced from its uses, and may not match
+ // the type of the composite member it is inserted into. OpCompositeInsert
+ // requires an exact match, so bitcast the pointer to the member type.
+ Register ObjReg = I.getOperand(3).getReg();
+ SPIRVTypeInst ObjType = GR.getSPIRVTypeForVReg(ObjReg);
+ if (ObjType && ObjType.isPointer()) {
+ SPIRVTypeInst MemberType = getCompositeMemberType(ResType, I, 4);
+ if (MemberType && MemberType != ObjType && MemberType.isPointer()) {
+ Register CastReg =
+ createVirtualRegister(MemberType, &GR, MRI, *I.getMF());
+ BuildMI(BB, I, I.getDebugLoc(), TII.get(SPIRV::OpBitcast))
+ .addDef(CastReg)
+ .addUse(GR.getSPIRVTypeID(MemberType))
+ .addUse(ObjReg)
+ .constrainAllUses(TII, TRI, RBI);
+ ObjReg = CastReg;
+ }
+ }
auto MIB = BuildMI(BB, I, I.getDebugLoc(), TII.get(SPIRV::OpCompositeInsert))
.addDef(ResVReg)
.addUse(GR.getSPIRVTypeID(ResType))
// object to insert
- .addUse(I.getOperand(3).getReg())
+ .addUse(ObjReg)
// composite to insert into
.addUse(I.getOperand(2).getReg());
for (unsigned i = 4; i < I.getNumOperands(); i++)
@@ -5001,14 +5050,34 @@ bool SPIRVInstructionSelector::selectExtractVal(Register ResVReg,
SPIRV::AccessQualifier::ReadWrite, false);
GR.assignSPIRVTypeToVReg(ResType, ResVReg, *I.getMF());
}
+ // The pointee type of an extracted pointer is deduced from its uses, and may
+ // not match the type of the composite member it is extracted from. In that
+ // case, extract the member as is and bitcast it to the expected type.
+ Register CompositeReg = I.getOperand(2).getReg();
+ Register ExtractReg = ResVReg;
+ SPIRVTypeInst ExtractType = ResType;
+ if (ResType.isPointer()) {
+ SPIRVTypeInst MemberType =
+ getCompositeMemberType(GR.getSPIRVTypeForVReg(CompositeReg), I, 3);
+ if (MemberType && MemberType != ResType && MemberType.isPointer()) {
+ ExtractType = MemberType;
+ ExtractReg = createVirtualRegister(MemberType, &GR, MRI, *I.getMF());
+ }
+ }
MachineBasicBlock &BB = *I.getParent();
auto MIB = BuildMI(BB, I, I.getDebugLoc(), TII.get(SPIRV::OpCompositeExtract))
- .addDef(ResVReg)
- .addUse(GR.getSPIRVTypeID(ResType))
- .addUse(I.getOperand(2).getReg());
+ .addDef(ExtractReg)
+ .addUse(GR.getSPIRVTypeID(ExtractType))
+ .addUse(CompositeReg);
for (unsigned i = 3; i < I.getNumOperands(); i++)
MIB.addImm(foldImm(I.getOperand(i), MRI));
MIB.constrainAllUses(TII, TRI, RBI);
+ if (ExtractReg != ResVReg)
+ BuildMI(BB, I, I.getDebugLoc(), TII.get(SPIRV::OpBitcast))
+ .addDef(ResVReg)
+ .addUse(GR.getSPIRVTypeID(ResType))
+ .addUse(ExtractReg)
+ .constrainAllUses(TII, TRI, RBI);
return true;
}
diff --git a/llvm/test/CodeGen/SPIRV/instructions/composite-ptr-member-bitcast.ll b/llvm/test/CodeGen/SPIRV/instructions/composite-ptr-member-bitcast.ll
new file mode 100644
index 0000000000000..a89222d42f656
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/instructions/composite-ptr-member-bitcast.ll
@@ -0,0 +1,67 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; The pointee type of a pointer extracted from, or inserted into, a composite
+; is deduced from its uses and can differ from the pointee type of the
+; composite member. OpCompositeExtract and OpCompositeInsert require the object
+; type to match the member type exactly, so the pointer is bitcast.
+
+; CHECK-DAG: %[[#Char:]] = OpTypeInt 8 0
+; CHECK-DAG: %[[#Int:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#CharPtr:]] = OpTypePointer Function %[[#Char]]
+; CHECK-DAG: %[[#IntPtr:]] = OpTypePointer Function %[[#Int]]
+; CHECK-DAG: %[[#Struct:]] = OpTypeStruct %[[#CharPtr]] %[[#Char]]
+
+define internal { ptr, i8 } @ret_aggr() {
+ ret { ptr, i8 } { ptr null, i8 1 }
+}
+
+; CHECK: OpFunction
+; CHECK: %[[#Aggr:]] = OpFunctionCall %[[#Struct]]
+; CHECK: %[[#Member:]] = OpCompositeExtract %[[#CharPtr]] %[[#Aggr]] 0
+; CHECK: %[[#Ptr:]] = OpBitcast %[[#IntPtr]] %[[#Member]]
+; CHECK: OpLoad %[[#Int]] %[[#Ptr]]
+define spir_kernel void @extract(ptr addrspace(1) %out) {
+ %r = call { ptr, i8 } @ret_aggr()
+ %p = extractvalue { ptr, i8 } %r, 0
+ %v = load i32, ptr %p
+ store i32 %v, ptr addrspace(1) %out
+ ret void
+}
+
+define internal { i32, { ptr, i8 } } @ret_nested_aggr() {
+ ret { i32, { ptr, i8 } } { i32 0, { ptr, i8 } { ptr null, i8 1 } }
+}
+
+; CHECK: OpFunction
+; CHECK: %[[#Aggr:]] = OpFunctionCall %[[#]]
+; CHECK: %[[#Member:]] = OpCompositeExtract %[[#CharPtr]] %[[#Aggr]] 1 0
+; CHECK: %[[#Ptr:]] = OpBitcast %[[#IntPtr]] %[[#Member]]
+; CHECK: OpLoad %[[#Int]] %[[#Ptr]]
+define spir_kernel void @extract_nested(ptr addrspace(1) %out) {
+ %r = call { i32, { ptr, i8 } } @ret_nested_aggr()
+ %p = extractvalue { i32, { ptr, i8 } } %r, 1, 0
+ %v = load i32, ptr %p
+ store i32 %v, ptr addrspace(1) %out
+ ret void
+}
+
+define internal i8 @take_aggr({ ptr, i8 } %a) {
+ %p = extractvalue { ptr, i8 } %a, 0
+ %v = load i8, ptr %p
+ ret i8 %v
+}
+
+; CHECK: OpFunction
+; CHECK: %[[#Var:]] = OpVariable %[[#IntPtr]] Function
+; CHECK: %[[#Cast:]] = OpBitcast %[[#CharPtr]] %[[#Var]]
+; CHECK: OpCompositeInsert %[[#Struct]] %[[#Cast]] %[[#]] 0
+define spir_kernel void @insert(ptr addrspace(1) %out) {
+ %x = alloca i32
+ store i32 7, ptr %x
+ %a = insertvalue { ptr, i8 } poison, ptr %x, 0
+ %b = insertvalue { ptr, i8 } %a, i8 1, 1
+ %r = call i8 @take_aggr({ ptr, i8 } %b)
+ store i8 %r, ptr addrspace(1) %out
+ ret void
+}
More information about the llvm-commits
mailing list