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
