[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