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

>From c0ca1b3bcd09f491d23a3efd42f4433f08788061 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Sat, 18 Jul 2026 10:47:04 -0700
Subject: [PATCH 1/3] [CIR] Add x86_64 aggregate calling-convention lowering

Extend the x86_64 SysV classifier bridge in CallConvLowering to handle
struct and array aggregates alongside the scalars it already supports.
A struct maps to an llvm::abi record (field offsets and the
CanPassInRegisters flag from the module's record-layout metadata) and an
array maps to an llvm::abi array.  The resulting ArgInfo is Direct (a
register-friendly coerced type the rewriter flattens) or Indirect (sret
for returns, byval or byref for arguments).  CIRABIRewriteContext already
performs these rewrites, so this only feeds it the aggregate
classifications.

Unions, packed and over-aligned records, empty-for-ABI records, and
_BitInt stay on the errorNYI path, so an unsupported signature fails
cleanly rather than being misclassified.  All-float aggregates are
included too.  Their SysV class coerces to an SSE vector the bridge does
not yet map, so rather than pass the aggregate unchanged the classifier
reports the coercion as NYI.

The byval and sret argument attributes still carry the CIR record type.
Converting that to the LLVM type in LowerToLLVM is a separate change, so
the byval and sret tests check the CIR output only, while the direct and
flatten tests also check the lowered LLVM IR.
---
 .../Transforms/CallConvLoweringPass.cpp       | 213 ++++++++++++++----
 .../abi-lowering/x86_64-aggregate-nyi.cir     |  82 +++++++
 .../abi-lowering/x86_64-struct-direct.cir     |  87 +++++++
 .../abi-lowering/x86_64-struct-indirect.cir   |  40 ++++
 4 files changed, 382 insertions(+), 40 deletions(-)
 create mode 100644 
clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
 create mode 100644 
clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir
 create mode 100644 
clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp 
b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index bb5b194ee0433..2d2b630bf8c74 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -62,21 +62,60 @@ namespace mlir {
 namespace {
 
 
//===----------------------------------------------------------------------===//
-// x86_64 System V classifier bridge (scalar types)
+// x86_64 System V classifier bridge (scalar and struct/array types)
 //
-// Maps scalar CIR types to llvm::abi::Type, runs the LLVM ABI Lowering
-// Library's SysV x86_64 classifier, and converts the result back into the
+// 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.  Only integer / pointer / bool / f32 / f64 signatures are handled;
-// aggregates and other leaf types are reported NYI by classifyX86_64Function
+// consumes.  Integer / pointer / bool / f32 / f64 scalars and struct / array
+// aggregates are handled; unions, `_BitInt`, `_Complex`, vectors, wider
+// floats, and empty-for-ABI records are reported NYI by classifyX86_64Function
 // so an unsupported signature fails the pass instead of being misclassified.
 
//===----------------------------------------------------------------------===//
 
-/// The scalar CIR types the x86_64 bridge handles.  A regular integer up to
-/// 64 bits, pointer, bool, void, f32, or f64 is a single-register Direct or
-/// Extend argument.  `_BitInt`, `__int128`, and wider/other types need 
coercion
-/// or indirect passing, which this scalar bridge does not do.
-static bool isSupportedScalarType(mlir::Type ty) {
+/// Proxy for the AST notion of an empty record (a C++ empty class): CIRGen
+/// materializes an empty class as a single padding byte, so a record whose
+/// members are all single-byte i8 arrays is empty for ABI purposes.  A real
+/// `char` array of size > 1 is data, not padding.  The bridge only uses this
+/// to reject empty records -- classifying them as Ignore is reported NYI in
+/// classifyX86_64Function.  A struct that merely contains an empty record is
+/// rejected too, but through the member recursion in isSupportedType.
+static bool recordIsEmptyForABI(cir::RecordType recTy) {
+  for (mlir::Type m : recTy.getMembers()) {
+    auto arr = dyn_cast<cir::ArrayType>(m);
+    if (!arr)
+      return false;
+    auto elt = dyn_cast<cir::IntType>(arr.getElementType());
+    if (!elt || elt.getWidth() != 8 || arr.getSize() != 1)
+      return false;
+  }
+  return true;
+}
+
+/// Whether a struct's declared argument-passing kind (from the module's
+/// record-layout metadata) allows it to be passed in registers.  A record with
+/// no layout entry (e.g. an anonymous struct) has no C++ non-trivial reason to
+/// be forced to memory, so it defaults to can-pass-in-registers.
+static bool recordCanPassInRegs(ModuleOp module, cir::RecordType recTy) {
+  mlir::StringAttr name = recTy.getName();
+  if (!name)
+    return true;
+  auto dict = module->getAttrOfType<DictionaryAttr>(
+      cir::CIRDialect::getRecordLayoutsAttrName());
+  auto layout =
+      dict ? dict.getAs<cir::RecordLayoutAttr>(name) : cir::RecordLayoutAttr();
+  if (!layout)
+    return true;
+  return layout.getArgPassingKind() == cir::ArgPassingKind::CanPassInRegs;
+}
+
+/// The CIR types the x86_64 bridge handles.  Scalars: a regular integer up to
+/// 64 bits, pointer, bool, void, f32, or f64.  Aggregates: a complete struct
+/// whose fields are all themselves supported, or an array of a supported
+/// element type.  `_BitInt`, `__int128`, unions, `_Complex`, vectors, wider
+/// floats, and empty-for-ABI records are not handled and are reported NYI at
+/// the reject() choke point in classifyX86_64Function.
+static bool isSupportedType(mlir::Type ty) {
   // A pointer is only handled in the default address space (null) or an
   // already-lowered target address space.  A LangAddressSpaceAttr must be
   // lowered before this pass, so reject it rather than silently dropping it.
@@ -87,6 +126,19 @@ static bool isSupportedScalarType(mlir::Type ty) {
     return true;
   if (auto intTy = dyn_cast<cir::IntType>(ty))
     return !intTy.getIsBitInt() && intTy.getWidth() <= 64;
+  if (auto arrTy = dyn_cast<cir::ArrayType>(ty))
+    return isSupportedType(arrTy.getElementType());
+  if (auto recTy = dyn_cast<cir::RecordType>(ty)) {
+    // Unions, packed / padded records, and empty-for-ABI records each need
+    // classification this bridge does not implement (a union widen fixup,
+    // pad-aware eightbyte classification, and Ignore respectively), so reject
+    // them here and report NYI rather than misclassify.
+    if (recTy.isUnion() || !recTy.isComplete() || recTy.getPacked() ||
+        recTy.getPadded() || recordIsEmptyForABI(recTy))
+      return false;
+    return llvm::all_of(recTy.getMembers(),
+                        [](mlir::Type m) { return isSupportedType(m); });
+  }
   return false;
 }
 
@@ -107,15 +159,28 @@ static mlir::Type abiTypeToCIR(const llvm::abi::Type *ty, 
MLIRContext *ctx) {
       .Case([&](const llvm::abi::PointerType *) {
         return cir::PointerType::get(cir::VoidType::get(ctx));
       })
+      .Case([&](const llvm::abi::RecordType *recTy) -> mlir::Type {
+        SmallVector<mlir::Type> fieldTypes;
+        fieldTypes.reserve(recTy->getFields().size());
+        for (const auto &field : recTy->getFields()) {
+          mlir::Type fieldCIR = abiTypeToCIR(field.FieldType, ctx);
+          if (!fieldCIR)
+            return nullptr;
+          fieldTypes.push_back(fieldCIR);
+        }
+        return cir::StructType::get(ctx, fieldTypes, /*packed=*/false,
+                                    /*padded=*/false, /*is_class=*/false);
+      })
       .Default([](const llvm::abi::Type *) -> mlir::Type { return nullptr; });
 }
 
-/// Map a scalar CIR type to an llvm::abi::Type.  classifyX86_64Function
-/// pre-filters the signature, so only the scalar types handled here can
+/// Map a CIR type to an llvm::abi::Type.  classifyX86_64Function pre-filters
+/// the signature, so only the scalar and struct/array types handled here can
 /// reach this function.
 static const llvm::abi::Type *mapCIRType(mlir::Type type,
                                          mlir::abi::ABITypeMapper &typeMapper,
-                                         const DataLayout &dl) {
+                                         const DataLayout &dl,
+                                         ModuleOp module) {
   llvm::abi::TypeBuilder &tb = typeMapper.getTypeBuilder();
   return llvm::TypeSwitch<mlir::Type, const llvm::abi::Type *>(type)
       .Case([&](cir::IntType intTy) {
@@ -147,17 +212,54 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
         return tb.getFloatType(llvm::APFloat::IEEEdouble(),
                                llvm::Align(dl.getTypeABIAlignment(type)));
       })
+      .Case([&](cir::ArrayType arrTy) {
+        const llvm::abi::Type *elemAbi =
+            mapCIRType(arrTy.getElementType(), typeMapper, dl, module);
+        return tb.getArrayType(elemAbi, arrTy.getSize(),
+                               dl.getTypeSizeInBits(type).getFixedValue());
+      })
+      .Case([&](cir::RecordType recTy) -> const llvm::abi::Type * {
+        // isSupportedType rejects unions, packed / padded, and empty-for-ABI
+        // records, so this handles a plain struct: map each field at its
+        // naturally-aligned offset.
+        SmallVector<llvm::abi::FieldInfo> fields;
+        fields.reserve(recTy.getMembers().size());
+        uint64_t offsetBits = 0;
+        for (mlir::Type fieldTy : recTy.getMembers()) {
+          const llvm::abi::Type *mappedField =
+              mapCIRType(fieldTy, typeMapper, dl, module);
+          offsetBits =
+              llvm::alignTo(offsetBits, dl.getTypeABIAlignment(fieldTy) * 8);
+          fields.push_back(llvm::abi::FieldInfo(mappedField, offsetBits));
+          offsetBits += dl.getTypeSizeInBits(fieldTy).getFixedValue();
+        }
+        llvm::abi::RecordFlags flags = llvm::abi::RecordFlags::None;
+        if (recordCanPassInRegs(module, recTy))
+          flags = flags | llvm::abi::RecordFlags::CanPassInRegisters;
+        return tb.getRecordType(fields,
+                                llvm::TypeSize::getFixed(
+                                    
dl.getTypeSizeInBits(type).getFixedValue()),
+                                llvm::Align(dl.getTypeABIAlignment(type)),
+                                llvm::abi::StructPacking::Default,
+                                /*BaseClasses=*/{}, /*VirtualBaseClasses=*/{},
+                                flags);
+      })
       .Default([](mlir::Type) -> const llvm::abi::Type * {
         llvm_unreachable(
             "mapCIRType: type not pre-filtered by classifyX86_64Function");
       });
 }
 
-/// Convert an llvm::abi::ArgInfo for a scalar type into the ArgClassification
-/// consumed by CIRABIRewriteContext.
+/// Convert an llvm::abi::ArgInfo into the ArgClassification consumed by
+/// CIRABIRewriteContext.
 ///
-/// Direct: every scalar this bridge maps passes as-is, so no coercion type
-/// is needed (nullptr means "same as the original CIR type").
+/// Direct: a scalar passes as-is (nullptr coercion means "same as the
+/// original CIR type").  A struct or array is coerced to a register-friendly
+/// type; getDirect keeps canFlatten set so the rewriter can split a
+/// multi-field coerced struct into individual wire arguments.  If the
+/// classifier picks a coercion this bridge cannot represent (e.g. an SSE
+/// <2 x float> vector), std::nullopt is returned so the caller reports NYI
+/// rather than silently passing the aggregate unchanged.
 ///
 /// Extend: bool or a sub-register integer needs a signext/zeroext attribute.
 /// Every ArgInfo::getExtend() call site in the x86_64 classifier
@@ -165,20 +267,27 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
 /// so a non-integer, non-bool origTy here would mean the classifier
 /// disagreed with its own source -- asserted rather than silently handled.
 ///
-/// Indirect: needed once records/_BitInt/vectors are supported (sret,
-/// byval, and large _BitInt all classify Indirect), but
-/// X86_64TargetInfo::getIndirectResult()/getIndirectReturnResult() only
-/// return Indirect for aggregates or _BitInt, neither of which the
-/// scalar-only type set this bridge accepts can produce.  Unreachable until
-/// a later PR adds those types and the record/vector/complex/int type gate
-/// that belongs here.
+/// Indirect: an aggregate that does not fit in registers is passed via a
+/// pointer (sret for returns, byval for arguments).
 ///
-/// Ignore: a void return has no register or stack slot.
-static ArgClassification convertABIArgInfo(const llvm::abi::ArgInfo &info,
-                                           MLIRContext *ctx,
-                                           mlir::Type origTy) {
-  if (info.isDirect())
-    return ArgClassification::getDirect(nullptr);
+/// Ignore: a void return has no register or stack slot.  (Empty-for-ABI
+/// records are rejected by isSupportedType, so they never reach here.)
+static std::optional<ArgClassification>
+convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
+                  mlir::Type origTy) {
+  if (info.isDirect()) {
+    // A scalar passes as-is; only an aggregate carries a coercion type.
+    if (!origTy || !isa<cir::RecordType, cir::ArrayType>(origTy))
+      return ArgClassification::getDirect(nullptr);
+    // An aggregate must coerce to a type this bridge can represent.  A coerce
+    // this bridge cannot map (an SSE vector, or a nested type it does not
+    // handle) yields a null type; report that as NYI instead of leaving the
+    // aggregate as an unchanged by-value record.
+    mlir::Type coerced = abiTypeToCIR(info.getCoerceToType(), ctx);
+    if (!coerced)
+      return std::nullopt;
+    return ArgClassification::getDirect(coerced);
+  }
   if (info.isExtend()) {
     if (origTy && isa<cir::BoolType>(origTy))
       return ArgClassification::getExtend(nullptr, info.isSignExt());
@@ -187,17 +296,21 @@ static ArgClassification convertABIArgInfo(const 
llvm::abi::ArgInfo &info,
     mlir::Type extendedTy = abiTypeToCIR(info.getCoerceToType(), ctx);
     return ArgClassification::getExtend(extendedTy, info.isSignExt());
   }
+  if (info.isIndirect())
+    return ArgClassification::getIndirect(info.getIndirectAlign(),
+                                          info.getIndirectByVal());
   assert(info.isIgnore() && "Unexpected classification");
   return ArgClassification::getIgnore();
 }
 
 /// Classify a cir.func for x86_64 SysV using the LLVM ABI library.  Returns
-/// std::nullopt and emits an NYI error if the signature uses a type the scalar
-/// bridge does not handle yet.
+/// std::nullopt and emits an NYI error if the signature uses a type the bridge
+/// does not handle yet.
 static std::optional<FunctionClassification>
 classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
                        mlir::abi::ABITypeMapper &typeMapper,
-                       const llvm::abi::TargetInfo &targetInfo) {
+                       const llvm::abi::TargetInfo &targetInfo,
+                       ModuleOp module) {
   MLIRContext *ctx = func->getContext();
   cir::FuncType fnTy = func.getFunctionType();
   mlir::Type retCIR = fnTy.getReturnType();
@@ -205,7 +318,7 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout 
&dl,
   bool voidRet = isa<cir::VoidType>(retCIR);
 
   auto reject = [&](mlir::Type t) -> bool {
-    if (isSupportedScalarType(t))
+    if (isSupportedType(t))
       return false;
     func.emitOpError()
         << "x86_64 calling-convention lowering not yet implemented for type "
@@ -220,23 +333,43 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout 
&dl,
 
   const llvm::abi::Type *retAbi =
       voidRet ? typeMapper.getTypeBuilder().getVoidType()
-              : mapCIRType(retCIR, typeMapper, dl);
+              : mapCIRType(retCIR, typeMapper, dl, module);
   SmallVector<const llvm::abi::Type *> argAbi;
   for (mlir::Type a : fnTy.getInputs())
-    argAbi.push_back(mapCIRType(a, typeMapper, dl));
+    argAbi.push_back(mapCIRType(a, typeMapper, dl, module));
 
   std::unique_ptr<llvm::abi::FunctionInfo> fi =
       llvm::abi::FunctionInfo::create(llvm::CallingConv::C, retAbi, argAbi);
   targetInfo.computeInfo(*fi);
 
+  // convertABIArgInfo returns nullopt when the classifier picks a coercion
+  // this bridge cannot represent (e.g. an SSE vector coerce for an all-float
+  // aggregate).  Report it as NYI rather than emitting a wrong signature.
+  auto nyiCoercion = [&](mlir::Type t) {
+    func.emitOpError() << "x86_64 calling-convention lowering not yet "
+                          "implemented for the ABI coercion of type "
+                       << t;
+  };
+
   FunctionClassification fc;
   mlir::Type origRet = voidRet ? mlir::Type() : retCIR;
-  fc.returnInfo = convertABIArgInfo(fi->getReturnInfo(), ctx, origRet);
+  std::optional<ArgClassification> retAc =
+      convertABIArgInfo(fi->getReturnInfo(), ctx, origRet);
+  if (!retAc) {
+    nyiCoercion(retCIR);
+    return std::nullopt;
+  }
+  fc.returnInfo = *retAc;
   auto inputs = fnTy.getInputs();
   for (unsigned i = 0, e = fi->arg_size(); i < e; ++i) {
     mlir::Type origArg = i < inputs.size() ? inputs[i] : mlir::Type();
-    fc.argInfos.push_back(
-        convertABIArgInfo(fi->getArgInfo(i).Info, ctx, origArg));
+    std::optional<ArgClassification> ac =
+        convertABIArgInfo(fi->getArgInfo(i).Info, ctx, origArg);
+    if (!ac) {
+      nyiCoercion(origArg);
+      return std::nullopt;
+    }
+    fc.argInfos.push_back(*ac);
   }
   return fc;
 }
@@ -347,7 +480,7 @@ void CallConvLoweringPass::runOnOperation() {
   moduleOp.walk([&](cir::FuncOp f) {
     std::optional<FunctionClassification> fc;
     if (x86Target)
-      fc = classifyX86_64Function(f, dl, *x86TypeMapper, *x86Target);
+      fc = classifyX86_64Function(f, dl, *x86TypeMapper, *x86Target, moduleOp);
     else
       fc = classifyFunction(f, dl, target, classificationAttr);
     if (!fc) {
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
new file mode 100644
index 0000000000000..b67fa211afbce
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
@@ -0,0 +1,82 @@
+// RUN: not cir-opt %s -cir-call-conv-lowering=target=x86_64 2>&1 | FileCheck 
%s
+
+!s8i = !cir.int<s, 8>
+!s32i = !cir.int<s, 32>
+!u8i = !cir.int<u, 8>
+!u32i = !cir.int<u, 32>
+!rec_U = !cir.union<"U" {!s32i, !u32i}>
+!rec_P = !cir.struct<"P" packed {!s8i, !s32i}>
+!rec_Ov = !cir.struct<"Ov" padded {!s32i, !cir.array<!u8i x 12>}>
+!rec_E = !cir.struct<"E" {!cir.array<!u8i x 1>}>
+!rec_FF = !cir.struct<"FF" {!cir.float, !cir.float}>
+!rec_RetFF = !cir.struct<"RetFF" {!cir.float, !cir.float}>
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>,
+    #dlti.dl_entry<f32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<f64, dense<64>: vector<2xi64>>>
+} {
+
+  // A union is rejected: its register coercion needs a widen fixup.
+  cir.func @take_union(%arg0: !rec_U) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for type '!cir.union<"U"
+
+  // A packed struct is rejected: it needs pad-aware classification.
+  cir.func @take_packed(%arg0: !rec_P) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for type '!cir.struct<"P" packed
+
+  // A padded (over-aligned) struct is rejected: it needs pad-aware
+  // classification.
+  cir.func @take_padded(%arg0: !rec_Ov) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for type '!cir.struct<"Ov" padded
+
+  // An empty-for-ABI record is rejected: it needs Ignore classification.
+  cir.func @take_empty(%arg0: !rec_E) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for type '!cir.struct<"E"
+
+  // An all-float struct classifies to an SSE vector coerce this bridge does
+  // not represent, so it is reported NYI rather than passed unchanged.
+  cir.func @take_ff(%arg0: !rec_FF) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.struct<"FF"
+
+  // The same holds for an all-float array.
+  cir.func @take_farr(%arg0: !cir.array<!cir.float x 2>) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.array<!cir.float x 2>
+
+  // A three-float struct coerces to a record with a vector field; the
+  // unmappable field propagates out as NYI too.
+  cir.func @take_f3(%arg0: !cir.struct<"F3" {!cir.float, !cir.float, 
!cir.float}>) {
+    cir.return
+  }
+
+  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.struct<"F3"
+
+  // The unmappable-coercion check also covers the return value.
+  cir.func @ret_ff() -> !rec_RetFF {
+    %0 = cir.alloca "r" align(4) : !cir.ptr<!rec_RetFF>
+    %1 = cir.load %0 : !cir.ptr<!rec_RetFF>, !rec_RetFF
+    cir.return %1 : !rec_RetFF
+  }
+
+  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.struct<"RetFF"
+}
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir
new file mode 100644
index 0000000000000..57433552bfdea
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir
@@ -0,0 +1,87 @@
+// 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
+
+!s8i = !cir.int<s, 8>
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Pair = !cir.struct<"Pair" {!s32i, !s32i}>
+!rec_Two = !cir.struct<"Two" {!s64i, !s64i}>
+!rec_Mixed = !cir.struct<"Mixed" {!s8i, !s32i}>
+!rec_CharBuf = !cir.struct<"CharBuf" {!cir.array<!s8i x 4>}>
+
+module attributes {
+  cir.triple = "x86_64-unknown-linux-gnu",
+  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 two-int struct (8 bytes) is coerced to a single i64 register.
+  cir.func @take_pair(%arg0: !rec_Pair) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_pair(%arg0: !u64i)
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!u64i>
+  // CHECK:   cir.store %arg0, %[[SLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK:   %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!u64i> -> 
!cir.ptr<!rec_Pair>
+
+  // A two-i64 struct (16 bytes) is passed as two i64 registers (flattened).
+  cir.func @take_two(%arg0: !rec_Two) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_two(%arg0: !s64i, %arg1: !s64i)
+
+  // Mixed-alignment fields (i8 then i32) still fit one eightbyte: the i32 is
+  // placed at its aligned offset and the struct coerces to a single i64.
+  cir.func @take_mixed(%arg0: !rec_Mixed) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_mixed(%arg0: !u64i)
+
+  // A struct wrapping a small char array is data, not an empty record: the
+  // 4-byte struct coerces to a single i32.
+  cir.func @take_charbuf(%arg0: !rec_CharBuf) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_charbuf(%arg0: !u32i)
+
+  // A 12-byte array argument is passed as two eightbytes (i64 then i32).
+  cir.func @take_arr3(%arg0: !cir.array<!s32i x 3>) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_arr3(%arg0: !u64i, %arg1: !s32i)
+
+  // An anonymous struct has no record-layout entry, so it defaults to
+  // can-pass-in-registers and coerces like its named counterpart.
+  cir.func @take_anon(%arg0: !cir.struct<{!s32i, !s32i}>) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_anon(%arg0: !u64i)
+
+  // The call site coerces the record operand to the same i64.
+  cir.func @call_pair(%arg0: !rec_Pair) {
+    cir.call @take_pair(%arg0) : (!rec_Pair) -> ()
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @call_pair(%arg0: !u64i)
+  // CHECK:   cir.call @take_pair(%{{.*}}) : (!u64i) -> ()
+}
+
+// LLVM: define void @take_pair(i64 %{{.+}})
+// LLVM: define void @take_two(i64 %{{.+}}, i64 %{{.+}})
+// LLVM: define void @take_mixed(i64 %{{.+}})
+// LLVM: define void @take_charbuf(i32 %{{.+}})
+// LLVM: define void @take_arr3(i64 %{{.+}}, i32 %{{.+}})
+// LLVM: define void @take_anon(i64 %{{.+}})
+// LLVM: define void @call_pair(i64 %{{.+}})
+// LLVM:   call void @take_pair(i64 %{{.+}})
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
new file mode 100644
index 0000000000000..a2d5987863d49
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
@@ -0,0 +1,40 @@
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s
+
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {!s64i, !s64i, !s64i}>
+!rec_NoRegs = !cir.struct<"NoRegs" {!s32i, !s32i}>
+
+module attributes {
+  cir.record_layouts = {NoRegs = #cir.record_layout<
+    arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false,
+    record_align = 4>},
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // A 24-byte struct does not fit in registers: passed byval.
+  cir.func @take_big(%arg0: !rec_Big) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_big(%arg0: !cir.ptr<!rec_Big> {llvm.align = 8 
: i64, llvm.byval = !rec_Big, llvm.noalias, llvm.noundef})
+
+  // A 24-byte struct return uses an sret pointer argument.
+  cir.func @ret_big() -> !rec_Big {
+    %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_Big>
+    %1 = cir.load %0 : !cir.ptr<!rec_Big>, !rec_Big
+    cir.return %1 : !rec_Big
+  }
+
+  // CHECK: cir.func{{.*}} @ret_big(%arg0: !cir.ptr<!rec_Big> {llvm.align = 8 
: i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = !rec_Big, llvm.writable})
+
+  // A small struct that cannot pass in registers (non-trivial for ABI) is
+  // passed indirectly even though its size would otherwise fit a register.
+  cir.func @take_noregs(%arg0: !rec_NoRegs) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_noregs(%arg0: !cir.ptr<!rec_NoRegs> 
{llvm.align = 4 : i64, llvm.byref = !rec_NoRegs})
+}

>From faa4fae848df480c7e99b7a745d9ff1e4c1574cd Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 20 Jul 2026 08:02:25 -0700
Subject: [PATCH 2/3] [CIR] Address review comments on aggregate lowering

Simplify recordIsEmptyForABI to the two shapes an empty class takes, no
members or a single size-1 i8 array, which also stops a multi-field i8
record from being misreported as empty.  Replace the layout-dictionary
ternary in recordCanPassInRegs with an early return.  The coerced struct
built in abiTypeToCIR is a register tuple, not the source record.
---
 .../Transforms/CallConvLoweringPass.cpp       | 25 +++++++++++--------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp 
b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 2d2b630bf8c74..f0d718a372fe0 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -81,15 +81,16 @@ namespace {
 /// classifyX86_64Function.  A struct that merely contains an empty record is
 /// rejected too, but through the member recursion in isSupportedType.
 static bool recordIsEmptyForABI(cir::RecordType recTy) {
-  for (mlir::Type m : recTy.getMembers()) {
-    auto arr = dyn_cast<cir::ArrayType>(m);
-    if (!arr)
-      return false;
-    auto elt = dyn_cast<cir::IntType>(arr.getElementType());
-    if (!elt || elt.getWidth() != 8 || arr.getSize() != 1)
-      return false;
-  }
-  return true;
+  mlir::ArrayRef<mlir::Type> members = recTy.getMembers();
+  if (members.empty())
+    return true;
+  if (members.size() != 1)
+    return false;
+  auto arr = dyn_cast<cir::ArrayType>(members[0]);
+  if (!arr)
+    return false;
+  auto elt = dyn_cast<cir::IntType>(arr.getElementType());
+  return elt && elt.getWidth() == 8 && arr.getSize() == 1;
 }
 
 /// Whether a struct's declared argument-passing kind (from the module's
@@ -102,8 +103,9 @@ static bool recordCanPassInRegs(ModuleOp module, 
cir::RecordType recTy) {
     return true;
   auto dict = module->getAttrOfType<DictionaryAttr>(
       cir::CIRDialect::getRecordLayoutsAttrName());
-  auto layout =
-      dict ? dict.getAs<cir::RecordLayoutAttr>(name) : cir::RecordLayoutAttr();
+  if (!dict)
+    return true;
+  auto layout = dict.getAs<cir::RecordLayoutAttr>(name);
   if (!layout)
     return true;
   return layout.getArgPassingKind() == cir::ArgPassingKind::CanPassInRegs;
@@ -168,6 +170,7 @@ static mlir::Type abiTypeToCIR(const llvm::abi::Type *ty, 
MLIRContext *ctx) {
             return nullptr;
           fieldTypes.push_back(fieldCIR);
         }
+        // Coercion types are plain register tuples, not the source record.
         return cir::StructType::get(ctx, fieldTypes, /*packed=*/false,
                                     /*padded=*/false, /*is_class=*/false);
       })

>From cfd36f15573aca4e02107334d5b791c63f7b1729 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 20 Jul 2026 12:49:36 -0700
Subject: [PATCH 3/3] [CIR] Fix one-byte struct misclassified as empty

recordIsEmptyForABI rejected any record whose single member is an i8[1]
array as empty for ABI, but that is the layout of a real one-byte struct
like `struct S { char a[1]; }`, which SysV coerces to i8.  Remove the
helper and reject only what actually classifies as Ignore: a zero-field
record (a C empty struct).  The empty C++ class it was also meant to catch
is laid out as `padded {i8}`, already rejected by the padded check.  Add a
`{char[1]}` -> i8 test and NYI tests for both the padded and zero-field
empty records.

Also address review nits: rename the `module` parameter to `modOp`, use
ordered CHECK in the aggregate-NYI test, and give the struct-direct and
struct-indirect tests bodies that read a field so the coerce prologue, the
byval load inserted at entry, and the record access are visible.
---
 .../Transforms/CallConvLoweringPass.cpp       | 62 +++++++------------
 .../abi-lowering/x86_64-aggregate-nyi.cir     | 30 ++++++---
 .../abi-lowering/x86_64-struct-direct.cir     | 57 +++++++++++++----
 .../abi-lowering/x86_64-struct-indirect.cir   | 19 ++++--
 4 files changed, 103 insertions(+), 65 deletions(-)

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp 
b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index f0d718a372fe0..e1414d3b311e2 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -69,39 +69,20 @@ namespace {
 // dialect-agnostic mlir::abi::FunctionClassification that CIRABIRewriteContext
 // consumes.  Integer / pointer / bool / f32 / f64 scalars and struct / array
 // aggregates are handled; unions, `_BitInt`, `_Complex`, vectors, wider
-// floats, and empty-for-ABI records are reported NYI by classifyX86_64Function
-// so an unsupported signature fails the pass instead of being misclassified.
+// floats, and packed or padded records are reported NYI by
+// classifyX86_64Function so an unsupported signature fails the pass instead of
+// being misclassified.
 
//===----------------------------------------------------------------------===//
 
-/// Proxy for the AST notion of an empty record (a C++ empty class): CIRGen
-/// materializes an empty class as a single padding byte, so a record whose
-/// members are all single-byte i8 arrays is empty for ABI purposes.  A real
-/// `char` array of size > 1 is data, not padding.  The bridge only uses this
-/// to reject empty records -- classifying them as Ignore is reported NYI in
-/// classifyX86_64Function.  A struct that merely contains an empty record is
-/// rejected too, but through the member recursion in isSupportedType.
-static bool recordIsEmptyForABI(cir::RecordType recTy) {
-  mlir::ArrayRef<mlir::Type> members = recTy.getMembers();
-  if (members.empty())
-    return true;
-  if (members.size() != 1)
-    return false;
-  auto arr = dyn_cast<cir::ArrayType>(members[0]);
-  if (!arr)
-    return false;
-  auto elt = dyn_cast<cir::IntType>(arr.getElementType());
-  return elt && elt.getWidth() == 8 && arr.getSize() == 1;
-}
-
 /// Whether a struct's declared argument-passing kind (from the module's
 /// record-layout metadata) allows it to be passed in registers.  A record with
 /// no layout entry (e.g. an anonymous struct) has no C++ non-trivial reason to
 /// be forced to memory, so it defaults to can-pass-in-registers.
-static bool recordCanPassInRegs(ModuleOp module, cir::RecordType recTy) {
+static bool recordCanPassInRegs(ModuleOp modOp, cir::RecordType recTy) {
   mlir::StringAttr name = recTy.getName();
   if (!name)
     return true;
-  auto dict = module->getAttrOfType<DictionaryAttr>(
+  auto dict = modOp->getAttrOfType<DictionaryAttr>(
       cir::CIRDialect::getRecordLayoutsAttrName());
   if (!dict)
     return true;
@@ -115,8 +96,8 @@ static bool recordCanPassInRegs(ModuleOp module, 
cir::RecordType recTy) {
 /// 64 bits, pointer, bool, void, f32, or f64.  Aggregates: a complete struct
 /// whose fields are all themselves supported, or an array of a supported
 /// element type.  `_BitInt`, `__int128`, unions, `_Complex`, vectors, wider
-/// floats, and empty-for-ABI records are not handled and are reported NYI at
-/// the reject() choke point in classifyX86_64Function.
+/// floats, and packed or padded records are not handled and are reported NYI
+/// at the reject() choke point in classifyX86_64Function.
 static bool isSupportedType(mlir::Type ty) {
   // A pointer is only handled in the default address space (null) or an
   // already-lowered target address space.  A LangAddressSpaceAttr must be
@@ -131,12 +112,16 @@ static bool isSupportedType(mlir::Type ty) {
   if (auto arrTy = dyn_cast<cir::ArrayType>(ty))
     return isSupportedType(arrTy.getElementType());
   if (auto recTy = dyn_cast<cir::RecordType>(ty)) {
-    // Unions, packed / padded records, and empty-for-ABI records each need
-    // classification this bridge does not implement (a union widen fixup,
-    // pad-aware eightbyte classification, and Ignore respectively), so reject
-    // them here and report NYI rather than misclassify.
+    // Unions and packed / padded records each need classification this bridge
+    // does not implement (a union widen fixup and pad-aware eightbyte
+    // classification), so reject them here and report NYI rather than
+    // misclassify.  Empty-for-ABI records classify as Ignore, which is also
+    // deferred: a C empty struct is a zero-field record, and CIRGen lays out
+    // an empty C++ class as a single padded byte (caught by the padded check).
+    // A real one-byte struct such as `{char[1]}` has a field and is not
+    // padded, so it is classified normally.
     if (recTy.isUnion() || !recTy.isComplete() || recTy.getPacked() ||
-        recTy.getPadded() || recordIsEmptyForABI(recTy))
+        recTy.getPadded() || recTy.getMembers().empty())
       return false;
     return llvm::all_of(recTy.getMembers(),
                         [](mlir::Type m) { return isSupportedType(m); });
@@ -182,8 +167,7 @@ static mlir::Type abiTypeToCIR(const llvm::abi::Type *ty, 
MLIRContext *ctx) {
 /// reach this function.
 static const llvm::abi::Type *mapCIRType(mlir::Type type,
                                          mlir::abi::ABITypeMapper &typeMapper,
-                                         const DataLayout &dl,
-                                         ModuleOp module) {
+                                         const DataLayout &dl, ModuleOp modOp) 
{
   llvm::abi::TypeBuilder &tb = typeMapper.getTypeBuilder();
   return llvm::TypeSwitch<mlir::Type, const llvm::abi::Type *>(type)
       .Case([&](cir::IntType intTy) {
@@ -217,7 +201,7 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
       })
       .Case([&](cir::ArrayType arrTy) {
         const llvm::abi::Type *elemAbi =
-            mapCIRType(arrTy.getElementType(), typeMapper, dl, module);
+            mapCIRType(arrTy.getElementType(), typeMapper, dl, modOp);
         return tb.getArrayType(elemAbi, arrTy.getSize(),
                                dl.getTypeSizeInBits(type).getFixedValue());
       })
@@ -230,14 +214,14 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
         uint64_t offsetBits = 0;
         for (mlir::Type fieldTy : recTy.getMembers()) {
           const llvm::abi::Type *mappedField =
-              mapCIRType(fieldTy, typeMapper, dl, module);
+              mapCIRType(fieldTy, typeMapper, dl, modOp);
           offsetBits =
               llvm::alignTo(offsetBits, dl.getTypeABIAlignment(fieldTy) * 8);
           fields.push_back(llvm::abi::FieldInfo(mappedField, offsetBits));
           offsetBits += dl.getTypeSizeInBits(fieldTy).getFixedValue();
         }
         llvm::abi::RecordFlags flags = llvm::abi::RecordFlags::None;
-        if (recordCanPassInRegs(module, recTy))
+        if (recordCanPassInRegs(modOp, recTy))
           flags = flags | llvm::abi::RecordFlags::CanPassInRegisters;
         return tb.getRecordType(fields,
                                 llvm::TypeSize::getFixed(
@@ -313,7 +297,7 @@ static std::optional<FunctionClassification>
 classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
                        mlir::abi::ABITypeMapper &typeMapper,
                        const llvm::abi::TargetInfo &targetInfo,
-                       ModuleOp module) {
+                       ModuleOp modOp) {
   MLIRContext *ctx = func->getContext();
   cir::FuncType fnTy = func.getFunctionType();
   mlir::Type retCIR = fnTy.getReturnType();
@@ -336,10 +320,10 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout 
&dl,
 
   const llvm::abi::Type *retAbi =
       voidRet ? typeMapper.getTypeBuilder().getVoidType()
-              : mapCIRType(retCIR, typeMapper, dl, module);
+              : mapCIRType(retCIR, typeMapper, dl, modOp);
   SmallVector<const llvm::abi::Type *> argAbi;
   for (mlir::Type a : fnTy.getInputs())
-    argAbi.push_back(mapCIRType(a, typeMapper, dl, module));
+    argAbi.push_back(mapCIRType(a, typeMapper, dl, modOp));
 
   std::unique_ptr<llvm::abi::FunctionInfo> fi =
       llvm::abi::FunctionInfo::create(llvm::CallingConv::C, retAbi, argAbi);
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 b67fa211afbce..87692e1a3e5a2 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
@@ -7,7 +7,8 @@
 !rec_U = !cir.union<"U" {!s32i, !u32i}>
 !rec_P = !cir.struct<"P" packed {!s8i, !s32i}>
 !rec_Ov = !cir.struct<"Ov" padded {!s32i, !cir.array<!u8i x 12>}>
-!rec_E = !cir.struct<"E" {!cir.array<!u8i x 1>}>
+!rec_E = !cir.struct<"E" padded {!u8i}>
+!rec_E0 = !cir.struct<"E0" {}>
 !rec_FF = !cir.struct<"FF" {!cir.float, !cir.float}>
 !rec_RetFF = !cir.struct<"RetFF" {!cir.float, !cir.float}>
 
@@ -24,14 +25,14 @@ module attributes {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for type '!cir.union<"U"
+  // CHECK: not yet implemented for type '!cir.union<"U"
 
   // A packed struct is rejected: it needs pad-aware classification.
   cir.func @take_packed(%arg0: !rec_P) {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for type '!cir.struct<"P" packed
+  // CHECK: not yet implemented for type '!cir.struct<"P" packed
 
   // A padded (over-aligned) struct is rejected: it needs pad-aware
   // classification.
@@ -39,14 +40,23 @@ module attributes {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for type '!cir.struct<"Ov" padded
+  // CHECK: not yet implemented for type '!cir.struct<"Ov" padded
 
-  // An empty-for-ABI record is rejected: it needs Ignore classification.
+  // An empty C++ class is laid out as a single padded byte, so it is rejected
+  // by the padded check; its Ignore classification is deferred.
   cir.func @take_empty(%arg0: !rec_E) {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for type '!cir.struct<"E"
+  // CHECK: not yet implemented for type '!cir.struct<"E" padded
+
+  // A zero-field record (a C empty struct) also classifies as Ignore, which is
+  // deferred, so it is rejected rather than passed with the argument dropped.
+  cir.func @take_e0(%arg0: !rec_E0) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.struct<"E0"
 
   // An all-float struct classifies to an SSE vector coerce this bridge does
   // not represent, so it is reported NYI rather than passed unchanged.
@@ -54,14 +64,14 @@ module attributes {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.struct<"FF"
+  // CHECK: not yet implemented for the ABI coercion of type '!cir.struct<"FF"
 
   // The same holds for an all-float array.
   cir.func @take_farr(%arg0: !cir.array<!cir.float x 2>) {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.array<!cir.float x 2>
+  // CHECK: not yet implemented for the ABI coercion of type 
'!cir.array<!cir.float x 2>
 
   // A three-float struct coerces to a record with a vector field; the
   // unmappable field propagates out as NYI too.
@@ -69,7 +79,7 @@ module attributes {
     cir.return
   }
 
-  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.struct<"F3"
+  // CHECK: not yet implemented for the ABI coercion of type '!cir.struct<"F3"
 
   // The unmappable-coercion check also covers the return value.
   cir.func @ret_ff() -> !rec_RetFF {
@@ -78,5 +88,5 @@ module attributes {
     cir.return %1 : !rec_RetFF
   }
 
-  // CHECK-DAG: not yet implemented for the ABI coercion of type 
'!cir.struct<"RetFF"
+  // CHECK: not yet implemented for the ABI coercion of type 
'!cir.struct<"RetFF"
 }
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir
index 57433552bfdea..4d430f7da721d 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct.cir
@@ -10,6 +10,7 @@
 !rec_Two = !cir.struct<"Two" {!s64i, !s64i}>
 !rec_Mixed = !cir.struct<"Mixed" {!s8i, !s32i}>
 !rec_CharBuf = !cir.struct<"CharBuf" {!cir.array<!s8i x 4>}>
+!rec_Char1 = !cir.struct<"Char1" {!cir.array<!s8i x 1>}>
 
 module attributes {
   cir.triple = "x86_64-unknown-linux-gnu",
@@ -19,22 +20,45 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  // A two-int struct (8 bytes) is coerced to a single i64 register.
-  cir.func @take_pair(%arg0: !rec_Pair) {
-    cir.return
+  // A two-int struct (8 bytes) is coerced to a single i64 register.  The body
+  // reads a field, so the prologue that reconstitutes the record from the
+  // register is visible: the i64 is stored to a coerce slot, the slot is
+  // bitcast to the record pointer, and the record is loaded back.
+  cir.func @take_pair(%arg0: !rec_Pair) -> !s32i {
+    %0 = cir.alloca "p" align(4) : !cir.ptr<!rec_Pair>
+    cir.store %arg0, %0 : !rec_Pair, !cir.ptr<!rec_Pair>
+    %1 = cir.get_member %0[0] {name = "a"} : !cir.ptr<!rec_Pair> -> 
!cir.ptr<!s32i>
+    %2 = cir.load %1 : !cir.ptr<!s32i>, !s32i
+    cir.return %2 : !s32i
   }
 
-  // CHECK: cir.func{{.*}} @take_pair(%arg0: !u64i)
+  // CHECK: cir.func{{.*}} @take_pair(%arg0: !u64i) -> !s32i
   // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!u64i>
   // CHECK:   cir.store %arg0, %[[SLOT]] : !u64i, !cir.ptr<!u64i>
   // CHECK:   %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!u64i> -> 
!cir.ptr<!rec_Pair>
+  // CHECK:   %[[REC:.*]] = cir.load %[[CAST]] : !cir.ptr<!rec_Pair>, !rec_Pair
+  // CHECK:   %[[LOCAL:.*]] = cir.alloca "p" {{.*}} : !cir.ptr<!rec_Pair>
+  // CHECK:   cir.store %[[REC]], %[[LOCAL]] : !rec_Pair, !cir.ptr<!rec_Pair>
+  // CHECK:   %[[FLD:.*]] = cir.get_member %[[LOCAL]][0] {name = "a"} : 
!cir.ptr<!rec_Pair> -> !cir.ptr<!s32i>
+  // CHECK:   %{{.*}} = cir.load %[[FLD]] : !cir.ptr<!s32i>, !s32i
 
   // A two-i64 struct (16 bytes) is passed as two i64 registers (flattened).
-  cir.func @take_two(%arg0: !rec_Two) {
-    cir.return
+  // The body reads the second field: the two registers are written into a
+  // flattened coerce slot, reloaded as the record, then indexed.
+  cir.func @take_two(%arg0: !rec_Two) -> !s64i {
+    %0 = cir.alloca "t" align(8) : !cir.ptr<!rec_Two>
+    cir.store %arg0, %0 : !rec_Two, !cir.ptr<!rec_Two>
+    %1 = cir.get_member %0[1] {name = "b"} : !cir.ptr<!rec_Two> -> 
!cir.ptr<!s64i>
+    %2 = cir.load %1 : !cir.ptr<!s64i>, !s64i
+    cir.return %2 : !s64i
   }
 
-  // CHECK: cir.func{{.*}} @take_two(%arg0: !s64i, %arg1: !s64i)
+  // CHECK: cir.func{{.*}} @take_two(%arg0: !s64i, %arg1: !s64i) -> !s64i
+  // CHECK:   cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_anon_struct>
+  // CHECK:   cir.store %arg0, %{{.*}} : !s64i, !cir.ptr<!s64i>
+  // CHECK:   cir.store %arg1, %{{.*}} : !s64i, !cir.ptr<!s64i>
+  // CHECK:   %{{.*}} = cir.cast bitcast %{{.*}} : !cir.ptr<!rec_anon_struct> 
-> !cir.ptr<!rec_Two>
+  // CHECK:   %{{.*}} = cir.get_member %{{.*}}[1] {name = "b"} : 
!cir.ptr<!rec_Two> -> !cir.ptr<!s64i>
 
   // Mixed-alignment fields (i8 then i32) still fit one eightbyte: the i32 is
   // placed at its aligned offset and the struct coerces to a single i64.
@@ -52,6 +76,14 @@ module attributes {
 
   // CHECK: cir.func{{.*}} @take_charbuf(%arg0: !u32i)
 
+  // A one-byte struct wrapping a single-element char array is data, not an
+  // empty class: it coerces to a single i8.
+  cir.func @take_char1(%arg0: !rec_Char1) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_char1(%arg0: !s8i)
+
   // A 12-byte array argument is passed as two eightbytes (i64 then i32).
   cir.func @take_arr3(%arg0: !cir.array<!s32i x 3>) {
     cir.return
@@ -69,19 +101,20 @@ module attributes {
 
   // The call site coerces the record operand to the same i64.
   cir.func @call_pair(%arg0: !rec_Pair) {
-    cir.call @take_pair(%arg0) : (!rec_Pair) -> ()
+    %0 = cir.call @take_pair(%arg0) : (!rec_Pair) -> !s32i
     cir.return
   }
 
   // CHECK: cir.func{{.*}} @call_pair(%arg0: !u64i)
-  // CHECK:   cir.call @take_pair(%{{.*}}) : (!u64i) -> ()
+  // CHECK:   %{{.*}} = cir.call @take_pair(%{{.*}}) : (!u64i) -> !s32i
 }
 
-// LLVM: define void @take_pair(i64 %{{.+}})
-// LLVM: define void @take_two(i64 %{{.+}}, i64 %{{.+}})
+// LLVM: define i32 @take_pair(i64 %{{.+}})
+// LLVM: define i64 @take_two(i64 %{{.+}}, i64 %{{.+}})
 // LLVM: define void @take_mixed(i64 %{{.+}})
 // LLVM: define void @take_charbuf(i32 %{{.+}})
+// LLVM: define void @take_char1(i8 %{{.+}})
 // LLVM: define void @take_arr3(i64 %{{.+}}, i32 %{{.+}})
 // LLVM: define void @take_anon(i64 %{{.+}})
 // LLVM: define void @call_pair(i64 %{{.+}})
-// LLVM:   call void @take_pair(i64 %{{.+}})
+// LLVM:   %{{.+}} = call i32 @take_pair(i64 %{{.+}})
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
index a2d5987863d49..373e47b41b9fe 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
@@ -14,12 +14,23 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  // A 24-byte struct does not fit in registers: passed byval.
-  cir.func @take_big(%arg0: !rec_Big) {
-    cir.return
+  // A 24-byte struct does not fit in registers: passed byval.  The body reads
+  // a field, so the load the rewriter inserts at entry (turning the byval
+  // pointer back into the record value) is visible.
+  cir.func @take_big(%arg0: !rec_Big) -> !s64i {
+    %0 = cir.alloca "b" align(8) : !cir.ptr<!rec_Big>
+    cir.store %arg0, %0 : !rec_Big, !cir.ptr<!rec_Big>
+    %1 = cir.get_member %0[2] {name = "c"} : !cir.ptr<!rec_Big> -> 
!cir.ptr<!s64i>
+    %2 = cir.load %1 : !cir.ptr<!s64i>, !s64i
+    cir.return %2 : !s64i
   }
 
-  // CHECK: cir.func{{.*}} @take_big(%arg0: !cir.ptr<!rec_Big> {llvm.align = 8 
: i64, llvm.byval = !rec_Big, llvm.noalias, llvm.noundef})
+  // CHECK: cir.func{{.*}} @take_big(%arg0: !cir.ptr<!rec_Big> {llvm.align = 8 
: i64, llvm.byval = !rec_Big, llvm.noalias, llvm.noundef}) -> !s64i
+  // CHECK:   %[[VAL:.*]] = cir.load %arg0 : !cir.ptr<!rec_Big>, !rec_Big
+  // CHECK:   %[[LOCAL:.*]] = cir.alloca "b" {{.*}} : !cir.ptr<!rec_Big>
+  // CHECK:   cir.store %[[VAL]], %[[LOCAL]] : !rec_Big, !cir.ptr<!rec_Big>
+  // CHECK:   %[[FLD:.*]] = cir.get_member %[[LOCAL]][2] {name = "c"} : 
!cir.ptr<!rec_Big> -> !cir.ptr<!s64i>
+  // CHECK:   %{{.*}} = cir.load %[[FLD]] : !cir.ptr<!s64i>, !s64i
 
   // A 24-byte struct return uses an sret pointer argument.
   cir.func @ret_big() -> !rec_Big {

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

Reply via email to