https://github.com/adams381 updated 
https://github.com/llvm/llvm-project/pull/218457

>From 0354d3fd1de0d48c9566ae724abe8a7154f7dbf7 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 24 Aug 2026 09:23:34 -0700
Subject: [PATCH 1/2] [CIR] Lower records carrying a vptr for x86_64

A record carrying a vtable pointer could not be passed or returned by
value.  `isSupportedType` had no case for `!cir.vptr`, so the whole
signature was reported NYI.  That covers any class with a virtual
function or a virtual base.

Accept the type and map it to an `llvm::abi::PointerType`.

A record CIRGen also marks packed stays NYI, which is where a
polymorphic class with tail padding lands.

Assisted-by: Cursor / claude-opus-5
---
 .../Transforms/CallConvLoweringPass.cpp       |  36 ++-
 .../call-conv-lowering-x86_64-vptr.cpp        |  98 ++++++++
 .../abi-lowering/x86_64-aggregate-nyi.cir     |  10 +
 .../Transforms/abi-lowering/x86_64-vptr.cir   | 230 ++++++++++++++++++
 4 files changed, 361 insertions(+), 13 deletions(-)
 create mode 100644 clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp
 create mode 100644 clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp 
b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 70ad91dca6bc0..4bb466058f28a 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -73,13 +73,13 @@ namespace {
 // Maps CIR types to llvm::abi::Type, runs the LLVM ABI Lowering Library's
 // SysV x86_64 classifier, and converts the result back into the
 // dialect-agnostic mlir::abi::FunctionClassification that CIRABIRewriteContext
-// consumes.  Integer (including `_BitInt` up to 128 bits) / pointer / bool /
-// floating-point scalars are handled, as are struct / union / array 
aggregates,
-// `_Complex`, and a fixed-width vector whose width is a power of two.  Other
-// vectors, packed records, a padded record that holds data, a union no member
-// of which spans its declared size, and a union with an empty-record member 
are
-// reported NYI by classifyX86_64Function so an unsupported signature fails the
-// pass instead of being misclassified.
+// consumes.  Integer (including `_BitInt` up to 128 bits) / pointer / vtable
+// pointer / bool / floating-point scalars are handled, as are struct / union /
+// array aggregates, `_Complex`, and a fixed-width vector whose width is a
+// power of two.  Other vectors, packed records, a padded record that holds
+// data, a union no member of which spans its declared size, and a union with
+// an empty-record member are reported NYI by classifyX86_64Function so an
+// unsupported signature fails the pass instead of being misclassified.
 
//===----------------------------------------------------------------------===//
 
 /// Whether a struct's declared argument-passing kind (from the module's
@@ -117,12 +117,12 @@ static llvm::Align recordDeclaredAlign(ModuleOp modOp, 
cir::RecordType recTy,
 }
 
 /// The CIR types the x86_64 bridge handles.  Scalars: an integer up to 128
-/// bits (including `_BitInt` and `__int128`), pointer, bool, void, or any
-/// floating-point type.  Aggregates: a complete struct or union whose members
-/// are all themselves supported, or an array of a supported element type.
-/// Also a `_Complex`, or a fixed-width vector, of a supported element type.
-/// Everything else is reported NYI at the reject() choke point in
-/// classifyX86_64Function.
+/// bits (including `_BitInt` and `__int128`), pointer, vtable pointer, bool,
+/// void, or any floating-point type.  Aggregates: a complete struct or union
+/// whose members are all themselves supported, or an array of a supported
+/// element type.  Also a `_Complex`, or a fixed-width vector, of a supported
+/// element type.  Everything else is reported NYI at the reject() choke point
+/// in classifyX86_64Function.
 static bool isSupportedType(mlir::Type ty, const DataLayout &dl) {
   // A pointer is only handled in the default address space (null) or an
   // already-lowered target address space.  A LangAddressSpaceAttr must be
@@ -130,6 +130,10 @@ static bool isSupportedType(mlir::Type ty, const 
DataLayout &dl) {
   if (auto ptrTy = dyn_cast<cir::PointerType>(ty))
     return !ptrTy.getAddrSpace() ||
            mlir::isa<cir::TargetAddressSpaceAttr>(ptrTy.getAddrSpace());
+  // A vtable pointer takes no address-space parameter, so unlike a
+  // cir::PointerType there is nothing here to reject.
+  if (isa<cir::VPtrType>(ty))
+    return true;
   if (isa<cir::VoidType, cir::BoolType>(ty))
     return true;
   // Every CIR floating-point type carries the semantics the classifier
@@ -291,6 +295,12 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
                                  llvm::Align(dl.getTypeABIAlignment(type)),
                                  addrSpace);
       })
+      .Case([&](cir::VPtrType) {
+        // The default address space is the only one a vtable pointer can name,
+        // so there is none to read off the type.
+        return tb.getPointerType(dl.getTypeSizeInBits(type),
+                                 llvm::Align(dl.getTypeABIAlignment(type)));
+      })
       .Case([&](cir::BoolType) {
         return tb.getIntegerType(dl.getTypeSizeInBits(type),
                                  llvm::Align(dl.getTypeABIAlignment(type)),
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp 
b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp
new file mode 100644
index 0000000000000..671f69adfebc2
--- /dev/null
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp
@@ -0,0 +1,98 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fclangir \
+// RUN:   -fclangir-call-conv-lowering -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fclangir \
+// RUN:   -fclangir-call-conv-lowering -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefixes=LLVM,LLVM-CIR --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -emit-llvm %s 
-o %t.ll
+// RUN: FileCheck --check-prefixes=LLVM,LLVM-OGCG --input-file=%t.ll %s
+
+struct Poly { virtual void f(); };
+struct PolyLong { virtual void f(); long x; };
+struct PolyTwoInt { virtual void f(); int x, y; };
+struct PolyBig { virtual void f(); long x, y; };
+struct PolyDerived : PolyLong { long z; };
+struct HasPoly { PolyLong p; };
+struct VBase { long a; };
+struct VirtInherit : virtual VBase { long b; };
+
+// A class with a virtual function has a non-trivial copy constructor, so it is
+// passed by invisible reference whatever its eightbytes classify as.
+int takePoly(Poly v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z8takePoly4Polyi(%arg0: !cir.ptr<!rec_Poly> 
{llvm.align = 8 : i64, llvm.byref = !rec_Poly}{{.*}}, %arg1: !s32i {{.*}}) -> 
(!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z8takePoly4Polyi(ptr 
byref(%struct.Poly) align 8 %{{[^,]+}}, i32 noundef %{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 @_Z8takePoly4Polyi(ptr nofree 
noundef align 8 dead_on_return dereferenceable(8) %{{[^,]+}}, i32 noundef 
%{{[^,)]+}})
+
+int takePolyLong(PolyLong v) { return 0; }
+
+// CIR: cir.func {{.*}}@_Z12takePolyLong8PolyLong(%arg0: 
!cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.byref = 
!rec_PolyLong}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z12takePolyLong8PolyLong(ptr 
byref(%struct.PolyLong) align 8 %{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 @_Z12takePolyLong8PolyLong(ptr 
nofree noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
+
+// The copy constructor decides this, so it does not matter what the members
+// would have classified as on their own.
+int takePolyTwoInt(PolyTwoInt v) { return v.y; }
+
+// CIR: cir.func {{.*}}@_Z14takePolyTwoInt10PolyTwoInt(%arg0: 
!cir.ptr<!rec_PolyTwoInt> {llvm.align = 8 : i64, llvm.byref = 
!rec_PolyTwoInt}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z14takePolyTwoInt10PolyTwoInt(ptr 
byref(%struct.PolyTwoInt) align 8 %{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 @_Z14takePolyTwoInt10PolyTwoInt(ptr 
nofree noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
+
+// Past two eightbytes SysV says memory on its own, so the two rules agree 
here.
+int takePolyBig(PolyBig v) { return 0; }
+
+// CIR: cir.func {{.*}}@_Z11takePolyBig7PolyBig(%arg0: !cir.ptr<!rec_PolyBig> 
{llvm.align = 8 : i64, llvm.byref = !rec_PolyBig}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z11takePolyBig7PolyBig(ptr 
byref(%struct.PolyBig) align 8 %{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 @_Z11takePolyBig7PolyBig(ptr nofree 
noundef align 8 dead_on_return dereferenceable(24) %{{[^,)]+}})
+
+// The vtable pointer is inherited through the base subobject rather than
+// declared here.
+int takePolyDerived(PolyDerived v) { return 0; }
+
+// CIR: cir.func {{.*}}@_Z15takePolyDerived11PolyDerived(%arg0: 
!cir.ptr<!rec_PolyDerived> {llvm.align = 8 : i64, llvm.byref = 
!rec_PolyDerived}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 
@_Z15takePolyDerived11PolyDerived(ptr byref(%struct.PolyDerived) align 8 
%{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 
@_Z15takePolyDerived11PolyDerived(ptr nofree noundef align 8 dead_on_return 
dereferenceable(24) %{{[^,)]+}})
+
+// HasPoly declares no virtual function of its own, but its member carries the
+// vtable pointer and the non-trivial copy constructor with it.
+int takeHasPoly(HasPoly v) { return 0; }
+
+// CIR: cir.func {{.*}}@_Z11takeHasPoly7HasPoly(%arg0: !cir.ptr<!rec_HasPoly> 
{llvm.align = 8 : i64, llvm.byref = !rec_HasPoly}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z11takeHasPoly7HasPoly(ptr 
byref(%struct.HasPoly) align 8 %{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 @_Z11takeHasPoly7HasPoly(ptr nofree 
noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
+
+// A virtual base gives the class a vtable pointer for the base offset even
+// though it declares no virtual function of its own.
+int takeVirtInherit(VirtInherit v) { return 0; }
+
+// CIR: cir.func {{.*}}@_Z15takeVirtInherit11VirtInherit(%arg0: 
!cir.ptr<!rec_VirtInherit> {llvm.align = 8 : i64, llvm.byref = 
!rec_VirtInherit}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 
@_Z15takeVirtInherit11VirtInherit(ptr byref(%struct.VirtInherit) align 8 
%{{[^,)]+}})
+// LLVM-OGCG: define dso_local noundef i32 
@_Z15takeVirtInherit11VirtInherit(ptr nofree noundef align 8 dead_on_return 
dereferenceable(24) %{{[^,)]+}})
+
+// Returning the class writes through an sret slot the caller supplies.
+PolyLong makePolyLong() {
+  PolyLong p;
+  return p;
+}
+
+// CIR: cir.func {{.*}}@_Z12makePolyLongv(%arg0: !cir.ptr<!rec_PolyLong> 
{llvm.align = 8 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = 
!rec_PolyLong, llvm.writable}
+// LLVM: define dso_local void @_Z12makePolyLongv(ptr dead_on_unwind noalias 
writable sret(%struct.PolyLong) align 8 %{{[^,)]+}})
+
+PolyLong retPolyLong();
+
+int caller(int k) {
+  PolyLong p = retPolyLong();
+  return takePolyLong(p) + k;
+}
+
+// CIR: cir.func {{.*}}@_Z6calleri(%arg0: !s32i {{.*}}) -> (!s32i
+// CIR:   cir.call @_Z11retPolyLongv(%{{[0-9]+}}) : (!cir.ptr<!rec_PolyLong> 
{llvm.align = 8 : i64, llvm.dead_on_unwind, llvm.sret = !rec_PolyLong, 
llvm.writable}) -> ()
+// CIR:   cir.call @_Z12takePolyLong8PolyLong(%{{[0-9]+}}) : 
(!cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.byref = !rec_PolyLong}) -> 
(!s32i
+// LLVM: define dso_local noundef i32 @_Z6calleri(i32 noundef %{{[^,)]+}})
+// LLVM:   call void @_Z11retPolyLongv(ptr dead_on_unwind writable 
sret(%struct.PolyLong) align 8 %{{[^,)]+}})
+// LLVM-CIR:   call noundef i32 @_Z12takePolyLong8PolyLong(ptr 
byref(%struct.PolyLong) align 8 %{{[^,)]+}})
+// LLVM-OGCG:   call noundef i32 @_Z12takePolyLong8PolyLong(ptr nofree noundef 
align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
+
+// Returned, the class uses sret at its declared alignment.
+// CIR: cir.func private @_Z11retPolyLongv(!cir.ptr<!rec_PolyLong> {llvm.align 
= 8 : i64, llvm.dead_on_unwind, llvm.sret = !rec_PolyLong, llvm.writable})
+// LLVM: declare void @_Z11retPolyLongv(ptr dead_on_unwind writable 
sret(%struct.PolyLong) align 8)
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
index 22d8fb3d221b1..2354c1799791a 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
@@ -11,6 +11,7 @@
 !rec_SWrapsOverAligned = !cir.struct<"SWrapsOverAligned" {data !cir.double, 
data !rec_UOverAligned}>
 !rec_UEmptyLarge = !cir.union<"UEmptyLarge" {}, padding = {!cir.array<!u8i x 
32>}>
 !rec_P = !cir.struct<"P" packed {data !s8i, data !s32i}>
+!rec_PVPtr = !cir.struct<"PVPtr" packed {data !cir.vptr, data !s32i, pad 
!cir.array<!u8i x 4>}>
 !rec_Ov = !cir.struct<"Ov" {data !s32i, pad !cir.array<!u8i x 12>}>
 !rec_E = !cir.struct<"E" {pad !u8i}>
 !rec_EOver = !cir.struct<"EOver" {pad !cir.array<!u8i x 16>}>
@@ -94,6 +95,15 @@ module attributes {
 
   // CHECK: not yet implemented for type '!cir.struct<"P" packed
 
+  // A vtable pointer is classifiable on its own, but packed still rejects the
+  // record around it.  This is the shape a polymorphic class with tail padding
+  // lowers to, since CIRGen marks that packed.
+  cir.func @take_packed_vptr(%arg0: !rec_PVPtr) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.struct<"PVPtr" packed
+
   // A padded (over-aligned) struct is rejected: it needs pad-aware
   // classification.
   cir.func @take_padded(%arg0: !rec_Ov) {
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir
new file mode 100644
index 0000000000000..702a0ae451e8b
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir
@@ -0,0 +1,230 @@
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 -cir-to-llvm -o - 
2>/dev/null \
+// RUN:   | mlir-translate -mlir-to-llvmir --allow-unregistered-dialect \
+// RUN:   | FileCheck %s --check-prefix=LLVM
+
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_VP = !cir.struct<"VP" {data !cir.vptr}>
+!rec_VPLong = !cir.struct<"VPLong" {data !cir.vptr, data !s64i}>
+!rec_VPTwoInt = !cir.struct<"VPTwoInt" {data !cir.vptr, data !s32i, data 
!s32i}>
+!rec_LongVP = !cir.struct<"LongVP" {data !s64i, data !cir.vptr}>
+!rec_VPDouble = !cir.struct<"VPDouble" {data !cir.vptr, data !cir.double}>
+!rec_VPArr = !cir.struct<"VPArr" {data !cir.array<!cir.vptr x 2>}>
+!rec_UVP = !cir.union<"UVP" {data !cir.vptr, data !s64i}>
+!rec_VPBig = !cir.struct<"VPBig" {data !cir.vptr, data !s64i, data !s64i}>
+!rec_VPNoRegs = !cir.struct<"VPNoRegs" {data !cir.vptr, data !s64i}>
+
+module attributes {
+  cir.triple = "x86_64-unknown-linux-gnu",
+  cir.record_layouts = {
+    VP = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    VPLong = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    VPTwoInt = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    LongVP = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    VPDouble = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    VPArr = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    UVP = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    VPBig = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>,
+    VPNoRegs = #cir.record_layout<
+      arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = true,
+      record_align = 8>},
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // A lone vtable pointer is a scalar in its own right, and passes Direct in
+  // its natural type the way any other pointer does.
+  cir.func @take_bare_vptr(%arg0: !cir.vptr) -> !cir.vptr {
+    cir.return %arg0 : !cir.vptr
+  }
+
+  // CHECK: cir.func{{.*}} @take_bare_vptr(%arg0: !cir.vptr) -> !cir.vptr
+  // CHECK-NEXT: cir.return %arg0 : !cir.vptr
+  // LLVM: define ptr @take_bare_vptr(ptr %{{[^,)]+}})
+
+  // A record holding nothing but a vtable pointer fills one eightbyte, which
+  // classifies INTEGER and coerces to the pointer occupying it.
+  cir.func @take_vp(%arg0: !rec_VP) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp(%arg0: !cir.ptr<!void>)
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" align(8) : 
!cir.ptr<!cir.ptr<!void>>
+  // CHECK:   cir.store %arg0, %[[SLOT]] : !cir.ptr<!void>, 
!cir.ptr<!cir.ptr<!void>>
+  // CHECK:   %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : 
!cir.ptr<!cir.ptr<!void>> -> !cir.ptr<!rec_VP>
+  // LLVM: define void @take_vp(ptr %{{[^,)]+}})
+
+  // The vtable pointer owns the first eightbyte and the long owns the second,
+  // so the record passes as a pointer and an integer register.
+  cir.func @take_vp_long(%arg0: !rec_VPLong) -> !s64i {
+    %0 = cir.alloca "v" align(8) : !cir.ptr<!rec_VPLong>
+    cir.store %arg0, %0 : !rec_VPLong, !cir.ptr<!rec_VPLong>
+    %1 = cir.get_member %0[1] {name = "x"} : !cir.ptr<!rec_VPLong> -> 
!cir.ptr<!s64i>
+    %2 = cir.load %1 : !cir.ptr<!s64i>, !s64i
+    cir.return %2 : !s64i
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp_long(%arg0: !cir.ptr<!void>, %arg1: !s64i) 
-> !s64i
+  // CHECK:   cir.alloca "coerce" align(8) : !cir.ptr<!rec_anon_struct>
+  // CHECK:   %[[FLAT:.*]] = cir.alloca "coerce" align(8) : 
!cir.ptr<!rec_anon_struct>
+  // CHECK:   %[[E0:.*]] = cir.get_member %[[FLAT]][0] {{.*}} : 
!cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.ptr<!void>>
+  // CHECK:   cir.store %arg0, %[[E0]] : !cir.ptr<!void>, 
!cir.ptr<!cir.ptr<!void>>
+  // CHECK:   %[[E1:.*]] = cir.get_member %[[FLAT]][1] {{.*}} : 
!cir.ptr<!rec_anon_struct> -> !cir.ptr<!s64i>
+  // CHECK:   cir.store %arg1, %[[E1]] : !s64i, !cir.ptr<!s64i>
+  // CHECK:   %{{.*}} = cir.cast bitcast %{{.*}} : !cir.ptr<!rec_anon_struct> 
-> !cir.ptr<!rec_VPLong>
+  // LLVM: define i64 @take_vp_long(ptr %{{[^,]+}}, i64 %{{[^,)]+}})
+
+  // Two ints share the second eightbyte behind the vtable pointer, so that
+  // eightbyte coerces to i64 while the first stays a pointer.
+  cir.func @take_vp_two_int(%arg0: !rec_VPTwoInt) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp_two_int(%arg0: !cir.ptr<!void>, %arg1: 
!u64i)
+  // LLVM: define void @take_vp_two_int(ptr %{{[^,]+}}, i64 %{{[^,)]+}})
+
+  // A vtable pointer in the second eightbyte rather than the first.
+  cir.func @take_long_vp(%arg0: !rec_LongVP) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_long_vp(%arg0: !s64i, %arg1: !cir.ptr<!void>)
+  // LLVM: define void @take_long_vp(i64 %{{[^,]+}}, ptr %{{[^,)]+}})
+
+  cir.func @ret_long_vp() -> !rec_LongVP {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_LongVP>
+    %1 = cir.load %0 : !cir.ptr<!rec_LongVP>, !rec_LongVP
+    cir.return %1 : !rec_LongVP
+  }
+
+  // CHECK: cir.func{{.*}} @ret_long_vp() -> !rec_anon_struct{{[0-9]*}}
+  // LLVM: define { i64, ptr } @ret_long_vp()
+
+  // A double behind the vtable pointer makes the second eightbyte SSE, so the
+  // two eightbytes land in registers of different classes.
+  cir.func @take_vp_double(%arg0: !rec_VPDouble) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp_double(%arg0: !cir.ptr<!void>, %arg1: 
!cir.double)
+  // LLVM: define void @take_vp_double(ptr %{{[^,]+}}, double %{{[^,)]+}})
+
+  cir.func @ret_vp_double() -> !rec_VPDouble {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_VPDouble>
+    %1 = cir.load %0 : !cir.ptr<!rec_VPDouble>, !rec_VPDouble
+    cir.return %1 : !rec_VPDouble
+  }
+
+  // CHECK: cir.func{{.*}} @ret_vp_double() -> !rec_anon_struct{{[0-9]*}}
+  // LLVM: define { ptr, double } @ret_vp_double()
+
+  // Reached by recursing through the array element type.
+  cir.func @take_vp_arr(%arg0: !rec_VPArr) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp_arr(%arg0: !cir.ptr<!void>, %arg1: 
!cir.ptr<!void>)
+  // LLVM: define void @take_vp_arr(ptr %{{[^,]+}}, ptr %{{[^,)]+}})
+
+  // A union reduces to one member before classification, so the vtable pointer
+  // has to survive that reduction.
+  cir.func @take_union_vp(%arg0: !rec_UVP) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_union_vp(%arg0: !cir.ptr<!void>)
+  // LLVM: define void @take_union_vp(ptr %{{[^,)]+}})
+
+  // VPLong returned comes back in two registers.
+  cir.func @ret_vp_long() -> !rec_VPLong {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_VPLong>
+    %1 = cir.load %0 : !cir.ptr<!rec_VPLong>, !rec_VPLong
+    cir.return %1 : !rec_VPLong
+  }
+
+  // CHECK: cir.func{{.*}} @ret_vp_long() -> !rec_anon_struct
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" align(8) : 
!cir.ptr<!rec_VPLong>
+  // CHECK:   %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : 
!cir.ptr<!rec_VPLong> -> !cir.ptr<!rec_anon_struct>
+  // LLVM: define { ptr, i64 } @ret_vp_long()
+
+  // A single eightbyte comes back in the one return register holding it.
+  cir.func @ret_vp() -> !rec_VP {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_VP>
+    %1 = cir.load %0 : !cir.ptr<!rec_VP>, !rec_VP
+    cir.return %1 : !rec_VP
+  }
+
+  // CHECK: cir.func{{.*}} @ret_vp() -> !cir.ptr<!void>
+  // LLVM: define ptr @ret_vp()
+
+  // Past two eightbytes SysV says memory whatever the eightbytes hold.  A
+  // record eligible for registers reaches memory by the byval path, not the
+  // byref path a cannot-pass-in-registers record takes.
+  cir.func @take_vp_big(%arg0: !rec_VPBig) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp_big(%arg0: !cir.ptr<!rec_VPBig> 
{llvm.align = 8 : i64, llvm.byval = !rec_VPBig, llvm.noalias, llvm.noundef})
+  // LLVM: define void @take_vp_big(ptr noalias noundef byval(%struct.VPBig) 
align 8 %{{[^,)]+}})
+
+  // Returned at that size it uses sret instead.
+  cir.func @ret_vp_big() -> !rec_VPBig {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_VPBig>
+    %1 = cir.load %0 : !cir.ptr<!rec_VPBig>, !rec_VPBig
+    cir.return %1 : !rec_VPBig
+  }
+
+  // CHECK: cir.func{{.*}} @ret_vp_big(%arg0: !cir.ptr<!rec_VPBig> {llvm.align 
= 8 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = !rec_VPBig, 
llvm.writable})
+  // LLVM: define void @ret_vp_big(ptr dead_on_unwind noalias writable 
sret(%struct.VPBig) align 8 %{{[^,)]+}})
+
+  // A declared argument-passing kind of cannot-pass-in-registers sends the
+  // record to memory whatever its eightbytes classify as, which is the path
+  // every polymorphic C++ class takes.
+  cir.func @take_vp_no_regs(%arg0: !rec_VPNoRegs) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_vp_no_regs(%arg0: !cir.ptr<!rec_VPNoRegs> 
{llvm.align = 8 : i64, llvm.byref = !rec_VPNoRegs})
+  // LLVM: define void @take_vp_no_regs(ptr byref(%struct.VPNoRegs) align 8 
%{{[^,)]+}})
+
+  // The same record returned takes sret rather than byref.
+  cir.func @ret_vp_no_regs() -> !rec_VPNoRegs {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_VPNoRegs>
+    %1 = cir.load %0 : !cir.ptr<!rec_VPNoRegs>, !rec_VPNoRegs
+    cir.return %1 : !rec_VPNoRegs
+  }
+
+  // CHECK: cir.func{{.*}} @ret_vp_no_regs(%arg0: !cir.ptr<!rec_VPNoRegs> 
{llvm.align = 8 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = 
!rec_VPNoRegs, llvm.writable})
+  // LLVM: define void @ret_vp_no_regs(ptr dead_on_unwind noalias writable 
sret(%struct.VPNoRegs) align 8 %{{[^,)]+}})
+
+  // A call site is decomposed into the same two registers the callee now 
takes.
+  cir.func @caller(%arg0: !rec_VPLong) -> !s64i {
+    %0 = cir.call @take_vp_long(%arg0) : (!rec_VPLong) -> !s64i
+    cir.return %0 : !s64i
+  }
+
+  // CHECK: cir.func{{.*}} @caller(%arg0: !cir.ptr<!void>, %arg1: !s64i) -> 
!s64i
+  // CHECK:   %[[P:.*]] = cir.load %{{.*}} : !cir.ptr<!cir.ptr<!void>>, 
!cir.ptr<!void>
+  // CHECK:   %[[X:.*]] = cir.load %{{.*}} : !cir.ptr<!s64i>, !s64i
+  // CHECK:   cir.call @take_vp_long(%[[P]], %[[X]]) : (!cir.ptr<!void>, 
!s64i) -> !s64i
+  // LLVM: define i64 @caller(ptr %{{[^,]+}}, i64 %{{[^,)]+}})
+}

>From 4a6f8e1b91d8735c2d1e041416cd6cff048f0a23 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 24 Aug 2026 12:18:48 -0700
Subject: [PATCH 2/2] [CIR] Track the vtable pointer address-space gap with an
 assert

Andy pointed out on #218457 that "a vtable pointer is always the default
address space" is an implementation gap dressed up as an ABI fact:
cir::VPtrType has no address-space parameter, so a non-default address
space is unmodeled rather than impossible.  Matches the assert already on
VPtrType's case in LowerToLLVM.cpp.

Assisted-by: Cursor / claude-opus-5
---
 .../Dialect/Transforms/CallConvLoweringPass.cpp    | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp 
b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 4bb466058f28a..1d9d5d09c7c08 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -130,10 +130,13 @@ static bool isSupportedType(mlir::Type ty, const 
DataLayout &dl) {
   if (auto ptrTy = dyn_cast<cir::PointerType>(ty))
     return !ptrTy.getAddrSpace() ||
            mlir::isa<cir::TargetAddressSpaceAttr>(ptrTy.getAddrSpace());
-  // A vtable pointer takes no address-space parameter, so unlike a
-  // cir::PointerType there is nothing here to reject.
-  if (isa<cir::VPtrType>(ty))
+  // cir::VPtrType carries no address-space parameter yet, so its target
+  // address space cannot be checked here even though it is not always the
+  // default one.
+  if (isa<cir::VPtrType>(ty)) {
+    assert(!cir::MissingFeatures::addressSpace());
     return true;
+  }
   if (isa<cir::VoidType, cir::BoolType>(ty))
     return true;
   // Every CIR floating-point type carries the semantics the classifier
@@ -296,8 +299,9 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
                                  addrSpace);
       })
       .Case([&](cir::VPtrType) {
-        // The default address space is the only one a vtable pointer can name,
-        // so there is none to read off the type.
+        // cir::VPtrType carries no address-space parameter yet, so this
+        // always maps into the default one until that gap closes.
+        assert(!cir::MissingFeatures::addressSpace());
         return tb.getPointerType(dl.getTypeSizeInBits(type),
                                  llvm::Align(dl.getTypeABIAlignment(type)));
       })

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to