[clang] [mlir] [CIR] Honor the Direct coercion offset in x86_64 callconv lowering (PR #220724)
Adam Smith via cfe-commits
cfe-commits at lists.llvm.org
Wed Sep 9 07:02:56 PDT 2026
https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/220724
>From 1644b94b8bb92f43cda31c08323c58c7b7fd79e1 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 2 Sep 2026 13:40:05 -0700
Subject: [PATCH 1/8] [CIR] Honor the Direct coercion offset in x86_64 callconv
lowering
When a record's low eightbyte holds no field, the classifier passes it in one
register read from byte 8 rather than byte 0. CallConvLowering had no way to
represent that, so `convertABIArgInfo` rejected the whole signature as NYI.
`ArgClassification` now carries the offset and `emitCoercionToMemory` reads and
writes the coerced scalar through a `u8` `cir.ptr_stride` at that byte. The
offset-zero path is untouched.
A register-tuple coercion at an offset is refused rather than read from the
wrong bytes, a pair SysV cannot currently produce.
Supersedes #203640, which had the coercion mechanism but nothing calling it.
Assisted-by: Cursor / claude-opus-5
---
.../Transforms/CallConvLoweringPass.cpp | 42 +++---
.../TargetLowering/CIRABIRewriteContext.cpp | 113 +++++++++++-----
.../call-conv-lowering-x86_64-empty.cpp | 31 +++++
.../abi-lowering/x86_64-aggregate-nyi.cir | 10 --
.../x86_64-struct-direct-offset.cir | 123 ++++++++++++++++++
mlir/include/mlir/ABI/ABIRewriteContext.h | 17 ++-
6 files changed, 275 insertions(+), 61 deletions(-)
create mode 100644 clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir
diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 233b826853f76..242fda30343a1 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -473,14 +473,16 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
/// CIRABIRewriteContext.
///
/// Direct: the value passes in register(s). A coercion is forwarded in the
-/// three cases where the value has to be rebuilt on the wire: an aggregate
+/// four cases where the value has to be rebuilt on the wire: an aggregate
/// unpacked into the register(s) holding it, a scalar too wide for one register
-/// split into a tuple of them, and a scalar the classifier widens to fill its
-/// eightbyte. getDirect keeps canFlatten set so the rewriter can split a
-/// multi-field coerced struct into individual wire arguments. Any other scalar
-/// passes in its natural CIR type, which a null coercion denotes. A coercion
-/// this bridge cannot represent yields std::nullopt so the caller reports NYI
-/// rather than silently passing the value unchanged.
+/// split into a tuple of them, a scalar the classifier widens to fill its
+/// eightbyte, and a value whose live bytes start partway into its storage
+/// because a leading eightbyte holds no field (getDirectOffset). getDirect
+/// keeps canFlatten set so the rewriter can split a multi-field coerced
+/// struct into individual wire arguments. Any other scalar passes in its
+/// natural CIR type, which a null coercion denotes. A coercion this bridge
+/// cannot represent yields std::nullopt so the caller reports NYI rather than
+/// silently passing the value unchanged.
///
/// Extend: bool or a sub-register integer needs a signext/zeroext attribute.
/// The x86_64 classifier (llvm/lib/ABI/Targets/X86.cpp) only returns Extend
@@ -495,11 +497,7 @@ static std::optional<ArgClassification>
convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
mlir::Type origTy) {
if (info.isDirect()) {
- // The rewriter reads a coercion from the start of the value's storage, so a
- // classification naming an offset into it has no representation here. The
- // classifier names one when the low eightbyte holds no field.
- if (info.getDirectOffset())
- return std::nullopt;
+ unsigned offset = info.getDirectOffset();
// The classifier names a coerce type even where it matches the natural
// type, so a non-null coerce does not by itself mean a rewrite is needed.
const llvm::abi::Type *coerceAbi = info.getCoerceToType();
@@ -510,6 +508,10 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
coerceAbi && isa_and_present<cir::ComplexType, cir::VectorType>(origTy);
bool coerceIsRegisterTuple =
isa_and_present<llvm::abi::RecordType>(coerceAbi);
+ // A tuple coercion cannot be read at an offset, so refuse the pair rather
+ // than emit a read of the wrong bytes.
+ if (offset && coerceIsRegisterTuple)
+ return std::nullopt;
// Compare widths rather than identity: a coerce no wider than the natural
// type carries the same value and needs no rewrite.
auto origInt = dyn_cast_if_present<cir::IntType>(origTy);
@@ -520,19 +522,27 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
coerceInt->getSizeInBits().getFixedValue() > origInt.getWidth();
// Leaving the rest alone also avoids a lossy round trip: abiTypeToCIR
// drops the LongDoubleType wrapper and a pointer's pointee, so comparing a
- // scalar against its own coerce would report a difference that is not one.
- if (!isAggregate && !comparesAgainstCoerce && !coerceIsRegisterTuple &&
- !coerceWidensScalar)
+ // scalar against its own coerce would report a difference that is not
+ // one. Passing a value through reads it from byte 0, so one living at
+ // an offset cannot take this path.
+ if (!offset && !isAggregate && !comparesAgainstCoerce &&
+ !coerceIsRegisterTuple && !coerceWidensScalar)
return ArgClassification::getDirect(nullptr);
mlir::Type coerced = abiTypeToCIR(coerceAbi, ctx);
if (!coerced)
return std::nullopt;
+ // An offset would be lost here, since a coercion to the value's own type
+ // is indistinguishable from no coercion at all.
+ if (offset && coerced == origTy)
+ return std::nullopt;
// Coercing a value to the type it already has would add a memory round
// trip for nothing.
if (comparesAgainstCoerce && coerced == origTy)
return ArgClassification::getDirect(nullptr);
- return ArgClassification::getDirect(coerced);
+ return ArgClassification::getDirect(coerced, offset);
}
+ // An extended value is always read from byte 0 of its own storage, so
+ // there is no offset to honor here.
if (info.isExtend()) {
if (isa_and_present<cir::BoolType>(origTy))
return ArgClassification::getExtend(nullptr, info.isSignExt());
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index 8cf332bbbefa3..91efb28625f0f 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -311,14 +311,21 @@ static uint64_t coercionByteSize(mlir::Type ty, const mlir::DataLayout &dl) {
/// dominate every use of the coerced value and must be a block that ends up
/// inside the enclosing function's entry block after any later outlining.
///
+/// \p offset is where the coerced value sits within the larger of the two
+/// types, non-zero when the ABI passes a value in a register read from
+/// partway into its storage. The coerced type must be the smaller of the
+/// two, so one that can exceed the value it coerces, such as a multi-field
+/// register tuple, must not be given an offset.
+///
/// Any operations the helper creates are appended to \p createdOps so the
/// caller can pass them to replaceAllUsesExcept and avoid clobbering the
/// store's value operand when later rewiring the source value.
-mlir::Value
-emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
- mlir::Type dstTy, mlir::Value src, mlir::Block *slotBlock,
- const mlir::DataLayout &dl,
- SmallPtrSetImpl<mlir::Operation *> &createdOps) {
+mlir::Value emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
+ mlir::Type dstTy, mlir::Value src,
+ mlir::Block *slotBlock,
+ const mlir::DataLayout &dl,
+ SmallPtrSetImpl<mlir::Operation *> &createdOps,
+ unsigned offset = 0) {
mlir::Type srcTy = src.getType();
assert(srcTy != dstTy &&
"emitCoercion callers must pre-check that the types differ");
@@ -330,6 +337,19 @@ emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
? srcTy
: dstTy;
+ // Sizes are compared two ways on purpose: relative size in
+ // coercionByteSize terms, which is how slotTy was picked, and capacity in
+ // getTypeSize terms, which is what the alloca is given.
+ [[maybe_unused]] mlir::Type coercedTy = slotTy == srcTy ? dstTy : srcTy;
+ assert((offset == 0 ||
+ coercionByteSize(coercedTy, dl) < coercionByteSize(slotTy, dl)) &&
+ "a direct offset must land on the coerced side, the smaller one");
+ assert((offset == 0 ||
+ offset + dl.getTypeSize(coercedTy) <= dl.getTypeSize(slotTy)) &&
+ "coerce slot too small for offset access");
+ assert((offset == 0 || offset % dl.getTypeABIAlignment(coercedTy) == 0) &&
+ "a direct offset must be aligned for the coerced access");
+
auto slotPtrTy = cir::PointerType::get(slotTy);
auto srcPtrTy = cir::PointerType::get(srcTy);
auto dstPtrTy = cir::PointerType::get(dstTy);
@@ -344,25 +364,42 @@ emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
}
createdOps.insert(alloca);
+ // The alloca already has slotTy, so asking for that type returns it
+ // unchanged. Any other type is reached by a bitcast, preceded by a byte
+ // stride when the coerced value lives at an offset.
+ auto slotView = [&](mlir::Type wantTy,
+ cir::PointerType wantPtrTy) -> mlir::Value {
+ if (wantTy == slotTy)
+ return alloca;
+ mlir::Value base = alloca;
+ if (offset != 0) {
+ auto u8Ty =
+ cir::IntType::get(builder.getContext(), 8, /*isSigned=*/false);
+ auto u8PtrTy = cir::PointerType::get(u8Ty);
+ auto u8Base = cir::CastOp::create(builder, loc, u8PtrTy,
+ cir::CastKind::bitcast, alloca);
+ createdOps.insert(u8Base);
+ auto strideTy =
+ cir::IntType::get(builder.getContext(), 64, /*isSigned=*/true);
+ auto strideVal = cir::ConstantOp::create(
+ builder, loc, cir::IntAttr::get(strideTy, offset));
+ createdOps.insert(strideVal);
+ base = cir::PtrStrideOp::create(builder, loc, u8PtrTy, u8Base, strideVal);
+ createdOps.insert(base.getDefiningOp());
+ }
+ auto cast = cir::CastOp::create(builder, loc, wantPtrTy,
+ cir::CastKind::bitcast, base);
+ createdOps.insert(cast);
+ return cast;
+ };
+
// Store through a source-typed view of the slot.
- mlir::Value srcSlot = alloca;
- if (slotTy != srcTy) {
- auto srcCast = cir::CastOp::create(builder, loc, srcPtrTy,
- cir::CastKind::bitcast, alloca);
- createdOps.insert(srcCast);
- srcSlot = srcCast;
- }
+ mlir::Value srcSlot = slotView(srcTy, srcPtrTy);
auto store = cir::StoreOp::create(builder, loc, src, srcSlot);
createdOps.insert(store);
// Return a destination-typed view of the slot.
- if (slotTy != dstTy) {
- auto dstCast = cir::CastOp::create(builder, loc, dstPtrTy,
- cir::CastKind::bitcast, alloca);
- createdOps.insert(dstCast);
- return dstCast;
- }
- return alloca;
+ return slotView(dstTy, dstPtrTy);
}
/// Coerce \p src to type \p dstTy by going through memory and load the whole
@@ -371,9 +408,10 @@ emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Type dstTy, mlir::Value src,
mlir::Block *slotBlock, const mlir::DataLayout &dl,
- SmallPtrSetImpl<mlir::Operation *> &createdOps) {
- mlir::Value dstSlot =
- emitCoercionToMemory(builder, loc, dstTy, src, slotBlock, dl, createdOps);
+ SmallPtrSetImpl<mlir::Operation *> &createdOps,
+ unsigned offset = 0) {
+ mlir::Value dstSlot = emitCoercionToMemory(builder, loc, dstTy, src,
+ slotBlock, dl, createdOps, offset);
auto load = cir::LoadOp::create(builder, loc, dstSlot);
createdOps.insert(load);
return load;
@@ -383,9 +421,10 @@ mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
/// (e.g. call-site coercion where we don't replaceAllUsesExcept).
mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Type dstTy, mlir::Value src,
- mlir::Block *slotBlock, const mlir::DataLayout &dl) {
+ mlir::Block *slotBlock, const mlir::DataLayout &dl,
+ unsigned offset = 0) {
SmallPtrSet<mlir::Operation *, 4> ignored;
- return emitCoercion(builder, loc, dstTy, src, slotBlock, dl, ignored);
+ return emitCoercion(builder, loc, dstTy, src, slotBlock, dl, ignored, offset);
}
/// The block a coercion slot's alloca belongs at the start of.
@@ -414,8 +453,8 @@ mlir::Block *coercionSlotBlock(mlir::Operation *op) {
/// new (coerced) return type.
void insertReturnCoercion(mlir::FunctionOpInterface funcOp,
mlir::Type origRetTy, mlir::Type coercedRetTy,
- mlir::OpBuilder &builder,
- const mlir::DataLayout &dl) {
+ mlir::OpBuilder &builder, const mlir::DataLayout &dl,
+ unsigned offset) {
SmallVector<cir::ReturnOp> returns;
funcOp.walk([&](cir::ReturnOp r) { returns.push_back(r); });
for (cir::ReturnOp r : returns) {
@@ -427,7 +466,7 @@ void insertReturnCoercion(mlir::FunctionOpInterface funcOp,
builder.setInsertionPoint(r);
mlir::Value coerced =
emitCoercion(builder, r.getLoc(), coercedRetTy, origVal,
- &funcOp->getRegion(0).front(), dl);
+ &funcOp->getRegion(0).front(), dl, offset);
r->setOperand(0, coerced);
}
}
@@ -650,6 +689,9 @@ void insertArgCoercion(mlir::FunctionOpInterface funcOp,
Value finalVal = flatLoaded;
if (origTy != flatTy) {
SmallPtrSet<Operation *, 4> coercionOps;
+ assert(!ac.directOffset &&
+ "each field is read from slot offset 0 here, so a flattened "
+ "coercion cannot honor a direct offset");
finalVal = emitCoercion(builder, loc, origTy, flatLoaded, &entry, dl,
coercionOps);
flattenOps.insert(coercionOps.begin(), coercionOps.end());
@@ -674,8 +716,9 @@ void insertArgCoercion(mlir::FunctionOpInterface funcOp,
builder.setInsertionPointToStart(&entry);
SmallPtrSet<mlir::Operation *, 4> coercionOps;
- mlir::Value adapted = emitCoercion(builder, funcOp.getLoc(), oldArgTy,
- blockArg, &entry, dl, coercionOps);
+ mlir::Value adapted =
+ emitCoercion(builder, funcOp.getLoc(), oldArgTy, blockArg, &entry, dl,
+ coercionOps, ac.directOffset);
// Replace blockArg uses with the adapted value, except inside the
// helper ops we just created. This is critical: the StoreOp's value
@@ -1062,7 +1105,7 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
if (fc.returnInfo.kind == ArgKind::Direct && fc.returnInfo.coercedType &&
!oldResultTypes.empty() && fc.returnInfo.coercedType != origRetTy)
insertReturnCoercion(funcOp, origRetTy, fc.returnInfo.coercedType,
- builder, dl);
+ builder, dl, fc.returnInfo.directOffset);
mlir::Block &entry = body.front();
@@ -1224,6 +1267,9 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
// alloca when possible).
if (arg.getType() != flatTy) {
SmallPtrSet<mlir::Operation *, 4> coercionOps;
+ assert(!ac.directOffset &&
+ "each field is read from slot offset 0 here, so a flattened "
+ "coercion cannot honor a direct offset");
mlir::Value coercedPtr = emitCoercionToMemory(
builder, call.getLoc(), flatTy, arg, slotBlock, dl, coercionOps);
for (auto [f, fieldTy] : llvm::enumerate(flatTy.getMembers())) {
@@ -1249,7 +1295,7 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
} else if (ac.kind == ArgKind::Direct && ac.coercedType &&
arg.getType() != ac.coercedType) {
arg = emitCoercion(builder, call.getLoc(), ac.coercedType, arg, slotBlock,
- dl);
+ dl, ac.directOffset);
newArgs.push_back(arg);
} else if (ac.kind == ArgKind::Indirect) {
// byval hands the callee its own copy. byref must name the caller's
@@ -1317,8 +1363,9 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
// emit a coercion back to the original type for the call's existing uses.
if (returnNeedsCoercion) {
builder.setInsertionPointAfter(newCall);
- mlir::Value coercedBack = emitCoercion(builder, call.getLoc(), origRetTy,
- newCall.getResult(), slotBlock, dl);
+ mlir::Value coercedBack =
+ emitCoercion(builder, call.getLoc(), origRetTy, newCall.getResult(),
+ slotBlock, dl, fc.returnInfo.directOffset);
call.getResult().replaceAllUsesWith(coercedBack);
}
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
index 30999935ef9d0..8fa7fefec41f6 100644
--- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
@@ -24,6 +24,7 @@ struct EmptySecond { long a; Empty e; };
struct EmptySSE { double a; Empty e; };
struct FloatEmpty { float a; Empty e; };
struct FloatEmptyFirst { Empty e; float a; };
+struct HiWord { Empty e; long hi; };
struct alignas(32) Big32 {};
union UBits { unsigned : 3; };
union UNone {};
@@ -145,6 +146,36 @@ float takeFloatEmptyFirst(FloatEmptyFirst v) { return v.a; }
// CIR: cir.func {{.*}}@_Z19takeFloatEmptyFirst15FloatEmptyFirst(%arg0: !cir.double {{.*}}) -> (!cir.float
// LLVM: define dso_local noundef float @_Z19takeFloatEmptyFirst15FloatEmptyFirst(double %{{[^,]+}})
+// Here the empty member owns eightbyte 0 rather than eightbyte 1, so NoClass
+// cannot just be dropped: the coercion has to start at byte 8.
+long takeHiWord(HiWord v) { return v.hi; }
+
+// CIR: cir.func {{.*}}@_Z10takeHiWord6HiWord(%arg0: !s64i {{.*}}) -> (!s64i
+// LLVM: define dso_local noundef i64 @_Z10takeHiWord6HiWord(i64 %{{[^,]+}})
+// LLVM: %{{.+}} = getelementptr{{( inbounds)?}} i8, ptr %{{.+}}, i64 8
+// LLVM: store i64 %{{.+}}, ptr %{{.+}}, align 8
+
+// The same offset on the return side.
+HiWord giveHiWord(long hi) {
+ HiWord w;
+ w.hi = hi;
+ return w;
+}
+
+// CIR: cir.func {{.*}}@_Z10giveHiWordl(%arg0: !s64i {llvm.noundef} {{.*}}) -> !s64i
+// LLVM: define dso_local i64 @_Z10giveHiWordl(i64 noundef %{{[^,]+}})
+// LLVM: %[[RGEP:.+]] = getelementptr{{( inbounds)?}} i8, ptr %{{.+}}, i64 8
+// LLVM: %[[RVAL:.+]] = load i64, ptr %[[RGEP]], align 8
+// LLVM: ret i64 %[[RVAL]]
+
+// The caller's own signature is unaffected by the callee's coercion.
+long callerHiWord(long hi) {
+ return takeHiWord(giveHiWord(hi));
+}
+
+// CIR: cir.func {{.*}}@_Z12callerHiWordl(%arg0: !s64i {{.*}}) -> (!s64i
+// LLVM: define dso_local noundef i64 @_Z12callerHiWordl(i64 noundef %{{[^,]+}})
+
// Past two eightbytes SysV says memory whatever the content, so an empty class
// this size is passed indirectly at its declared alignment.
int takeBig32(Big32 v, int k) { return k; }
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 7da3a76abc494..b9e2d795e70b4 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
@@ -33,7 +33,6 @@
!rec_HoldsAllPad = !cir.struct<"HoldsAllPad" {data !s32i, empty !rec_E}>
!rec_EBits = !cir.struct<"EBits" {empty !u32i}>
!rec_HoldsEmptyBits = !cir.struct<"HoldsEmptyBits" {empty !rec_EBits, data !s32i}>
-!rec_LeadPad = !cir.struct<"LeadPad" {pad !cir.array<!u8i x 8>, data !s32i}>
!rec_Incomplete = !cir.struct<"Incomplete" incomplete>
module attributes {
@@ -196,15 +195,6 @@ module attributes {
// CHECK: not yet implemented for type '!cir.struct<"HoldsEmptyBits" {empty
- // With no field in the low eightbyte the classifier coerces the high one and
- // names an offset of 8 into the record, which the rewriter cannot express: it
- // reads a coercion from offset 0, so the argument would land in the padding.
- cir.func @take_lead_pad(%arg0: !rec_LeadPad) {
- cir.return
- }
-
- // CHECK: not yet implemented for the ABI coercion of type '!cir.struct<"LeadPad"
-
// A scalable vector has no size the eightbyte rules can read.
cir.func @take_scalable(%arg0: !cir.vector<[4] x !cir.float>) {
cir.return
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir
new file mode 100644
index 0000000000000..07132c8c308fc
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir
@@ -0,0 +1,123 @@
+// 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
+
+!u8i = !cir.int<u, 8>
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Empty = !cir.struct<"Empty" {pad !u8i}>
+!rec_HiWord = !cir.struct<"HiWord" {data !rec_Empty, data !s64i}>
+!rec_LeadPad = !cir.struct<"LeadPad" {pad !cir.array<!u8i x 8>, data !s32i}>
+
+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>>>
+} {
+
+ // HiWord is 16 bytes: a leading ABI-empty byte, then an i64 at offset 8.
+ // The low eightbyte carries no field (NO_CLASS), so the classifier passes
+ // the value in one register taken from byte 8.
+ cir.func @take_hiword(%arg0: !rec_HiWord) -> !s64i {
+ %0 = cir.alloca "h" align(8) : !cir.ptr<!rec_HiWord>
+ cir.store %arg0, %0 : !rec_HiWord, !cir.ptr<!rec_HiWord>
+ %1 = cir.get_member %0[1] {name = "hi"} : !cir.ptr<!rec_HiWord> -> !cir.ptr<!s64i>
+ %2 = cir.load %1 : !cir.ptr<!s64i>, !s64i
+ cir.return %2 : !s64i
+ }
+
+ // CHECK-LABEL: cir.func{{.*}} @take_hiword(%arg0: !s64i) -> !s64i
+ // CHECK: %[[SLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CHECK: %[[U8:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CHECK: %[[OFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CHECK: %[[GEP:.*]] = cir.ptr_stride %[[U8]], %[[OFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CHECK: %[[CAST:.*]] = cir.cast bitcast %[[GEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CHECK: cir.store %arg0, %[[CAST]] : !s64i, !cir.ptr<!s64i>
+ // CHECK: %{{.*}} = cir.load %[[SLOT]] : !cir.ptr<!rec_HiWord>, !rec_HiWord
+
+ // The return value is likewise coerced from byte 8 of the local aggregate.
+ cir.func @give_hiword() -> !rec_HiWord {
+ %0 = cir.alloca "h" align(8) : !cir.ptr<!rec_HiWord>
+ %1 = cir.load %0 : !cir.ptr<!rec_HiWord>, !rec_HiWord
+ cir.return %1 : !rec_HiWord
+ }
+
+ // CHECK-LABEL: cir.func{{.*}} @give_hiword() -> !s64i
+ // CHECK: %[[RSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CHECK: cir.store %{{.*}}, %[[RSLOT]] : !rec_HiWord, !cir.ptr<!rec_HiWord>
+ // CHECK: %[[RU8:.*]] = cir.cast bitcast %[[RSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CHECK: %[[ROFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CHECK: %[[RGEP:.*]] = cir.ptr_stride %[[RU8]], %[[ROFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CHECK: %[[RCAST:.*]] = cir.cast bitcast %[[RGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CHECK: %[[RVAL:.*]] = cir.load %[[RCAST]] : !cir.ptr<!s64i>, !s64i
+ // CHECK: cir.return %[[RVAL]] : !s64i
+
+ // The caller reconstructs the aggregate from the callee's i64 result, so
+ // the received register is written back to byte 8 of a slot.
+ cir.func @call_hiword() -> !s64i {
+ %0 = cir.call @give_hiword() : () -> !rec_HiWord
+ %1 = cir.alloca "r" align(8) : !cir.ptr<!rec_HiWord>
+ cir.store %0, %1 : !rec_HiWord, !cir.ptr<!rec_HiWord>
+ %2 = cir.get_member %1[1] {name = "hi"} : !cir.ptr<!rec_HiWord> -> !cir.ptr<!s64i>
+ %3 = cir.load %2 : !cir.ptr<!s64i>, !s64i
+ cir.return %3 : !s64i
+ }
+
+ // CHECK-LABEL: cir.func{{.*}} @call_hiword() -> !s64i
+ // CHECK: %[[CSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CHECK: %[[CRET:.*]] = cir.call @give_hiword() : () -> !s64i
+ // CHECK: %[[CU8:.*]] = cir.cast bitcast %[[CSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CHECK: %[[COFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CHECK: %[[CGEP:.*]] = cir.ptr_stride %[[CU8]], %[[COFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CHECK: %[[CCAST:.*]] = cir.cast bitcast %[[CGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CHECK: cir.store %[[CRET]], %[[CCAST]] : !s64i, !cir.ptr<!s64i>
+ // CHECK: %{{.*}} = cir.load %[[CSLOT]] : !cir.ptr<!rec_HiWord>, !rec_HiWord
+
+ // Passing an aggregate argument runs the extraction direction at the call
+ // site: the operand's byte 8 is read out to form the register.
+ cir.func @pass_hiword(%arg0: !rec_HiWord) {
+ %0 = cir.call @take_hiword(%arg0) : (!rec_HiWord) -> !s64i
+ cir.return
+ }
+
+ // CHECK-LABEL: cir.func{{.*}} @pass_hiword(%arg0: !s64i)
+ // CHECK: %[[PREC:.*]] = cir.load %{{.*}} : !cir.ptr<!rec_HiWord>, !rec_HiWord
+ // CHECK: %[[PSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CHECK: cir.store %[[PREC]], %[[PSLOT]] : !rec_HiWord, !cir.ptr<!rec_HiWord>
+ // CHECK: %[[PU8:.*]] = cir.cast bitcast %[[PSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CHECK: %[[POFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CHECK: %[[PGEP:.*]] = cir.ptr_stride %[[PU8]], %[[POFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CHECK: %[[PCAST:.*]] = cir.cast bitcast %[[PGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CHECK: %[[PVAL:.*]] = cir.load %[[PCAST]] : !cir.ptr<!s64i>, !s64i
+ // CHECK: %{{.*}} = cir.call @take_hiword(%[[PVAL]]) : (!s64i) -> !s64i
+
+ // The same offset-8 coercion from a leading pad field rather than a nested
+ // empty record. Only 4 live bytes in the high eightbyte, so i32 not i64.
+ cir.func @take_lead_pad(%arg0: !rec_LeadPad) -> !s32i {
+ %0 = cir.alloca "l" align(4) : !cir.ptr<!rec_LeadPad>
+ cir.store %arg0, %0 : !rec_LeadPad, !cir.ptr<!rec_LeadPad>
+ %1 = cir.get_member %0[1] {name = "data"} : !cir.ptr<!rec_LeadPad> -> !cir.ptr<!s32i>
+ %2 = cir.load %1 : !cir.ptr<!s32i>, !s32i
+ cir.return %2 : !s32i
+ }
+
+ // CHECK-LABEL: cir.func{{.*}} @take_lead_pad(%arg0: !s32i) -> !s32i
+ // CHECK: %[[LSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_LeadPad>
+ // CHECK: %[[LU8:.*]] = cir.cast bitcast %[[LSLOT]] : !cir.ptr<!rec_LeadPad> -> !cir.ptr<!u8i>
+ // CHECK: %[[LOFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CHECK: %[[LGEP:.*]] = cir.ptr_stride %[[LU8]], %[[LOFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CHECK: %[[LCAST:.*]] = cir.cast bitcast %[[LGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s32i>
+ // CHECK: cir.store %arg0, %[[LCAST]] : !s32i, !cir.ptr<!s32i>
+ // CHECK: %{{.*}} = cir.load %[[LSLOT]] : !cir.ptr<!rec_LeadPad>, !rec_LeadPad
+}
+
+// LLVM: define i64 @take_hiword(i64 %{{.+}})
+// LLVM: define i64 @give_hiword()
+// LLVM: define i64 @call_hiword()
+// LLVM: %{{.+}} = call i64 @give_hiword()
+// LLVM: define void @pass_hiword(i64 %{{.+}})
+// LLVM: %{{.+}} = call i64 @take_hiword(i64 %{{.+}})
+// LLVM: define i32 @take_lead_pad(i32 %{{.+}})
diff --git a/mlir/include/mlir/ABI/ABIRewriteContext.h b/mlir/include/mlir/ABI/ABIRewriteContext.h
index 8394aeccc75ce..c1c205e42b2e4 100644
--- a/mlir/include/mlir/ABI/ABIRewriteContext.h
+++ b/mlir/include/mlir/ABI/ABIRewriteContext.h
@@ -27,6 +27,8 @@
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Alignment.h"
+#include <cassert>
+
namespace mlir {
namespace abi {
@@ -75,6 +77,11 @@ struct ArgClassification {
/// For Indirect: whether the callee gets ownership (byval).
bool byVal = false;
+ /// For Direct with coercion: the byte offset within the original aggregate
+ /// at which the coerced value lives. Non-zero when the low eightbyte is
+ /// NO_CLASS and the value is carried in a later eightbyte (x86-64 SysV).
+ unsigned directOffset = 0;
+
/// Whether the value is passed as-is, so a rewriter can leave it alone.
/// Only an uncoerced Direct qualifies. Extend counts as needing a rewrite
/// even though it only adds an attribute, because the attribute changes
@@ -87,13 +94,19 @@ struct ArgClassification {
return kind == other.kind && coercedType == other.coercedType &&
indirectAlign == other.indirectAlign &&
signExtend == other.signExtend && canFlatten == other.canFlatten &&
- byVal == other.byVal;
+ byVal == other.byVal && directOffset == other.directOffset;
}
- static ArgClassification getDirect(Type coerced = nullptr) {
+ static ArgClassification getDirect(Type coerced = nullptr,
+ unsigned offset = 0) {
+ // isPassThrough reads only coercedType, so an offset with no coerced
+ // type to read at it would be silently ignored.
+ assert((!offset || coerced) &&
+ "a direct offset needs a coerced type to read at it");
ArgClassification c;
c.kind = ArgKind::Direct;
c.coercedType = coerced;
+ c.directOffset = offset;
return c;
}
>From 697007d9d5bee2b0937953d26d7d1c41c54208d6 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 2 Sep 2026 15:53:08 -0500
Subject: [PATCH 2/8] Update
clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
Taken verbatim.
Co-authored-by: Erich Keane <ekeane at nvidia.com>
---
.../Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index 91efb28625f0f..23db84f67e78f 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -340,7 +340,7 @@ mlir::Value emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
// Sizes are compared two ways on purpose: relative size in
// coercionByteSize terms, which is how slotTy was picked, and capacity in
// getTypeSize terms, which is what the alloca is given.
- [[maybe_unused]] mlir::Type coercedTy = slotTy == srcTy ? dstTy : srcTy;
+ [[maybe_unused]] mlir::Type coercedTy = ((slotTy == srcTy) ? dstTy : srcTy);
assert((offset == 0 ||
coercionByteSize(coercedTy, dl) < coercionByteSize(slotTy, dl)) &&
"a direct offset must land on the coerced side, the smaller one");
>From cd35d3473613e8293855d4489def57bebf20c0b6 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 3 Sep 2026 07:16:49 -0700
Subject: [PATCH 3/8] [CIR][NFC] Require the coercion offset to be passed
explicitly
Assisted-by: Cursor / claude-opus-5
---
.../CIR/Dialect/Transforms/CallConvLoweringPass.cpp | 6 +++---
.../TargetLowering/CIRABIRewriteContext.cpp | 13 +++++++------
mlir/include/mlir/ABI/ABIRewriteContext.h | 3 +--
mlir/lib/ABI/Targets/Test/TestTarget.cpp | 10 +++++-----
mlir/unittests/ABI/ABIRewriteContextTest.cpp | 8 ++++----
5 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 242fda30343a1..e967772802fcf 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -527,7 +527,7 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
// an offset cannot take this path.
if (!offset && !isAggregate && !comparesAgainstCoerce &&
!coerceIsRegisterTuple && !coerceWidensScalar)
- return ArgClassification::getDirect(nullptr);
+ return ArgClassification::getDirect(/*offset=*/0);
mlir::Type coerced = abiTypeToCIR(coerceAbi, ctx);
if (!coerced)
return std::nullopt;
@@ -538,8 +538,8 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
// Coercing a value to the type it already has would add a memory round
// trip for nothing.
if (comparesAgainstCoerce && coerced == origTy)
- return ArgClassification::getDirect(nullptr);
- return ArgClassification::getDirect(coerced, offset);
+ return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect(offset, coerced);
}
// An extended value is always read from byte 0 of its own storage, so
// there is no offset to honor here.
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index 23db84f67e78f..b2e6016e14e54 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -325,7 +325,7 @@ mlir::Value emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Block *slotBlock,
const mlir::DataLayout &dl,
SmallPtrSetImpl<mlir::Operation *> &createdOps,
- unsigned offset = 0) {
+ unsigned offset) {
mlir::Type srcTy = src.getType();
assert(srcTy != dstTy &&
"emitCoercion callers must pre-check that the types differ");
@@ -409,7 +409,7 @@ mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Type dstTy, mlir::Value src,
mlir::Block *slotBlock, const mlir::DataLayout &dl,
SmallPtrSetImpl<mlir::Operation *> &createdOps,
- unsigned offset = 0) {
+ unsigned offset) {
mlir::Value dstSlot = emitCoercionToMemory(builder, loc, dstTy, src,
slotBlock, dl, createdOps, offset);
auto load = cir::LoadOp::create(builder, loc, dstSlot);
@@ -422,7 +422,7 @@ mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
mlir::Type dstTy, mlir::Value src,
mlir::Block *slotBlock, const mlir::DataLayout &dl,
- unsigned offset = 0) {
+ unsigned offset) {
SmallPtrSet<mlir::Operation *, 4> ignored;
return emitCoercion(builder, loc, dstTy, src, slotBlock, dl, ignored, offset);
}
@@ -693,7 +693,7 @@ void insertArgCoercion(mlir::FunctionOpInterface funcOp,
"each field is read from slot offset 0 here, so a flattened "
"coercion cannot honor a direct offset");
finalVal = emitCoercion(builder, loc, origTy, flatLoaded, &entry, dl,
- coercionOps);
+ coercionOps, /*offset=*/0);
flattenOps.insert(coercionOps.begin(), coercionOps.end());
}
@@ -1270,8 +1270,9 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
assert(!ac.directOffset &&
"each field is read from slot offset 0 here, so a flattened "
"coercion cannot honor a direct offset");
- mlir::Value coercedPtr = emitCoercionToMemory(
- builder, call.getLoc(), flatTy, arg, slotBlock, dl, coercionOps);
+ mlir::Value coercedPtr =
+ emitCoercionToMemory(builder, call.getLoc(), flatTy, arg, slotBlock,
+ dl, coercionOps, /*offset=*/0);
for (auto [f, fieldTy] : llvm::enumerate(flatTy.getMembers())) {
mlir::Type fieldPtrTy = cir::PointerType::get(fieldTy);
auto fieldPtr =
diff --git a/mlir/include/mlir/ABI/ABIRewriteContext.h b/mlir/include/mlir/ABI/ABIRewriteContext.h
index c1c205e42b2e4..0004bb77b6801 100644
--- a/mlir/include/mlir/ABI/ABIRewriteContext.h
+++ b/mlir/include/mlir/ABI/ABIRewriteContext.h
@@ -97,8 +97,7 @@ struct ArgClassification {
byVal == other.byVal && directOffset == other.directOffset;
}
- static ArgClassification getDirect(Type coerced = nullptr,
- unsigned offset = 0) {
+ static ArgClassification getDirect(unsigned offset, Type coerced = nullptr) {
// isPassThrough reads only coercedType, so an offset with no coerced
// type to read at it would be silently ignored.
assert((!offset || coerced) &&
diff --git a/mlir/lib/ABI/Targets/Test/TestTarget.cpp b/mlir/lib/ABI/Targets/Test/TestTarget.cpp
index 0cfdb2eb3fdb0..66a6a29bd3e6f 100644
--- a/mlir/lib/ABI/Targets/Test/TestTarget.cpp
+++ b/mlir/lib/ABI/Targets/Test/TestTarget.cpp
@@ -69,7 +69,7 @@ ArgClassification classifyOne(Type type, const DataLayout &dl) {
Type i32Ty = IntegerType::get(type.getContext(), ExtendBelowBits);
return ArgClassification::getExtend(i32Ty, /*signExt=*/intTy.isSigned());
}
- return ArgClassification::getDirect();
+ return ArgClassification::getDirect(/*offset=*/0);
}
if (auto indexTy = dyn_cast<IndexType>(type)) {
@@ -78,11 +78,11 @@ ArgClassification classifyOne(Type type, const DataLayout &dl) {
Type i32Ty = IntegerType::get(type.getContext(), ExtendBelowBits);
return ArgClassification::getExtend(i32Ty, /*signExt=*/true);
}
- return ArgClassification::getDirect();
+ return ArgClassification::getDirect(/*offset=*/0);
}
if (isa<FloatType, VectorType, MemRefType>(type))
- return ArgClassification::getDirect();
+ return ArgClassification::getDirect(/*offset=*/0);
// For dialect-specific types: query DataLayout via
// DataLayoutTypeInterface. Types that don't implement the interface
@@ -99,7 +99,7 @@ ArgClassification classifyOne(Type type, const DataLayout &dl) {
uint64_t sizeInBytes = (sizeInBits.getFixedValue() + 7) / 8;
if (sizeInBytes <= IndirectCutoffBytes)
- return ArgClassification::getDirect();
+ return ArgClassification::getDirect(/*offset=*/0);
uint64_t alignBytes = dl.getTypeABIAlignment(type);
return ArgClassification::getIndirect(llvm::Align(alignBytes),
@@ -161,7 +161,7 @@ parseOne(DictionaryAttr argDict, function_ref<InFlightDiagnostic()> emitError) {
Type coerced;
if (auto t = argDict.getAs<TypeAttr>("coerced_type"))
coerced = t.getValue();
- auto c = ArgClassification::getDirect(coerced);
+ auto c = ArgClassification::getDirect(/*offset=*/0, coerced);
if (auto cf = argDict.getAs<BoolAttr>("can_flatten"))
c.canFlatten = cf.getValue();
return c;
diff --git a/mlir/unittests/ABI/ABIRewriteContextTest.cpp b/mlir/unittests/ABI/ABIRewriteContextTest.cpp
index 59a5307225e0d..e76c5f3821528 100644
--- a/mlir/unittests/ABI/ABIRewriteContextTest.cpp
+++ b/mlir/unittests/ABI/ABIRewriteContextTest.cpp
@@ -36,7 +36,7 @@ TEST(ABIRewriteContextTest, MockCanBeConstructedAndDestroyed) {
}
TEST(ABIRewriteContextTest, ArgClassificationDirect) {
- auto c = ArgClassification::getDirect();
+ auto c = ArgClassification::getDirect(/*offset=*/0);
EXPECT_EQ(c.kind, ArgKind::Direct);
EXPECT_EQ(c.coercedType, nullptr);
EXPECT_TRUE(c.canFlatten);
@@ -45,7 +45,7 @@ TEST(ABIRewriteContextTest, ArgClassificationDirect) {
TEST(ABIRewriteContextTest, ArgClassificationDirectWithType) {
MLIRContext mlirCtx;
auto i32 = IntegerType::get(&mlirCtx, 32);
- auto c = ArgClassification::getDirect(i32);
+ auto c = ArgClassification::getDirect(/*offset=*/0, i32);
EXPECT_EQ(c.kind, ArgKind::Direct);
EXPECT_EQ(c.coercedType, i32);
}
@@ -84,8 +84,8 @@ TEST(ABIRewriteContextTest, ArgClassificationExtend) {
TEST(ABIRewriteContextTest, FunctionClassificationHoldsReturnAndArgs) {
FunctionClassification fc;
- fc.returnInfo = ArgClassification::getDirect();
- fc.argInfos.push_back(ArgClassification::getDirect());
+ fc.returnInfo = ArgClassification::getDirect(/*offset=*/0);
+ fc.argInfos.push_back(ArgClassification::getDirect(/*offset=*/0));
fc.argInfos.push_back(ArgClassification::getIndirect(llvm::Align(8), true));
fc.argInfos.push_back(ArgClassification::getIgnore());
>From bbf7c29d6dc00f12baee4c31753d445766080630 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 3 Sep 2026 07:16:49 -0700
Subject: [PATCH 4/8] [CIR] Enable callconv lowering in the x86_64 C++ argument
test
Two cases here hit the coercion NYI this branch removes. The pass now
runs on the file.
The `LLVM` lines for those two stopped at the open paren and passed whatever
the pass did with the argument. Both prefixes now pin the coerced parameter
and the byte-8 access.
Assisted-by: Cursor / claude-opus-5
---
.../test/CIR/CodeGenCXX/x86_64-arguments.cpp | 26 ++++++++++++++-----
1 file changed, 19 insertions(+), 7 deletions(-)
diff --git a/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp b/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp
index 7270c36df7df6..acd6eb6a5264c 100644
--- a/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp
+++ b/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp
@@ -1,9 +1,6 @@
-// TODO(cir): drop -fno-clangir-call-conv-lowering once CallConvLowering
-// supports parameters of an empty or tag class and padded, packed, and
-// over-aligned record shapes.
-// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-unknown-unknown -fclangir -fno-clangir-call-conv-lowering -emit-cir %s -o %t.cir
+// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-unknown-unknown -fclangir -emit-cir %s -o %t.cir
// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
-// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-unknown-unknown -fclangir -fno-clangir-call-conv-lowering -emit-llvm %s -o %t-cir.ll
+// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-unknown-unknown -fclangir -emit-llvm %s -o %t-cir.ll
// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-unknown-unknown -emit-llvm %s -o %t.ll
// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
@@ -44,9 +41,17 @@ struct s3_0 {};
struct s3_1 { struct s3_0 a; long b; };
void f3(struct s3_1 x) {}
+// The empty member owns eightbyte 0, so the live long is the second eightbyte
+// and the coercion starts at byte 8.
// CIR-LABEL: cir.func {{.*}} @_Z2f34s3_1
-// LLVM-LABEL: define {{.*}} void @_Z2f34s3_1(
+// LLVM-LABEL: define {{.*}} void @_Z2f34s3_1(i64 %{{[^,)]+}})
+// LLVM: %[[SLOT:.+]] = alloca %struct.s3_1, align 8
+// LLVM: %[[HI:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
+// LLVM: store i64 %{{.+}}, ptr %[[HI]], align 8
// OGCG-LABEL: define {{.*}} void @_Z2f34s3_1(i64 %x.coerce)
+// OGCG: %[[SLOT:.+]] = alloca %struct.s3_1, align 8
+// OGCG: %[[HI:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
+// OGCG: store i64 %x.coerce, ptr %[[HI]], align 8
// Member data pointer and member function pointer.
struct s4 {};
@@ -121,9 +126,16 @@ namespace PR5179 {
const void *bar(B2 b2) { return b2.b1.pa; }
}
+// The empty base owns eightbyte 0, so the pointer is read from byte 8.
// CIR-LABEL: cir.func {{.*}} @_ZN6PR51793barENS_2B2E
-// LLVM-LABEL: define {{.*}} @_ZN6PR51793barENS_2B2E(
+// LLVM-LABEL: define {{.*}} ptr @_ZN6PR51793barENS_2B2E(ptr %{{[^,)]+}})
+// LLVM: %[[SLOT:.+]] = alloca %"struct.PR5179::B2", align 8
+// LLVM: %[[PA:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
+// LLVM: store ptr %{{.+}}, ptr %[[PA]], align 8
// OGCG-LABEL: define {{.*}} ptr @_ZN6PR51793barENS_2B2E(ptr %b2.coerce)
+// OGCG: %[[SLOT:.+]] = alloca %"struct.PR5179::B2", align 8
+// OGCG: %[[PA:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
+// OGCG: store ptr %b2.coerce, ptr %[[PA]], align 8
namespace test5 {
struct Xbase { };
>From ba2faf1b07cc5fe836f99fb5fab9c7cd75ef173b Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 3 Sep 2026 08:15:55 -0700
Subject: [PATCH 5/8] [CIR][NFC] Overload getDirect instead of defaulting its
arguments
Assisted-by: Cursor / claude-opus-5
---
clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp | 4 ++--
mlir/include/mlir/ABI/ABIRewriteContext.h | 6 +++++-
mlir/lib/ABI/Targets/Test/TestTarget.cpp | 8 ++++----
mlir/unittests/ABI/ABIRewriteContextTest.cpp | 6 +++---
4 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index e967772802fcf..c1ebd17e1e6f0 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -527,7 +527,7 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
// an offset cannot take this path.
if (!offset && !isAggregate && !comparesAgainstCoerce &&
!coerceIsRegisterTuple && !coerceWidensScalar)
- return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect();
mlir::Type coerced = abiTypeToCIR(coerceAbi, ctx);
if (!coerced)
return std::nullopt;
@@ -538,7 +538,7 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
// Coercing a value to the type it already has would add a memory round
// trip for nothing.
if (comparesAgainstCoerce && coerced == origTy)
- return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect();
return ArgClassification::getDirect(offset, coerced);
}
// An extended value is always read from byte 0 of its own storage, so
diff --git a/mlir/include/mlir/ABI/ABIRewriteContext.h b/mlir/include/mlir/ABI/ABIRewriteContext.h
index 0004bb77b6801..430ebe94bc5b6 100644
--- a/mlir/include/mlir/ABI/ABIRewriteContext.h
+++ b/mlir/include/mlir/ABI/ABIRewriteContext.h
@@ -97,7 +97,11 @@ struct ArgClassification {
byVal == other.byVal && directOffset == other.directOffset;
}
- static ArgClassification getDirect(unsigned offset, Type coerced = nullptr) {
+ static ArgClassification getDirect() {
+ return getDirect(/*offset=*/0, /*coerced=*/nullptr);
+ }
+
+ static ArgClassification getDirect(unsigned offset, Type coerced) {
// isPassThrough reads only coercedType, so an offset with no coerced
// type to read at it would be silently ignored.
assert((!offset || coerced) &&
diff --git a/mlir/lib/ABI/Targets/Test/TestTarget.cpp b/mlir/lib/ABI/Targets/Test/TestTarget.cpp
index 66a6a29bd3e6f..f36595ae52bb4 100644
--- a/mlir/lib/ABI/Targets/Test/TestTarget.cpp
+++ b/mlir/lib/ABI/Targets/Test/TestTarget.cpp
@@ -69,7 +69,7 @@ ArgClassification classifyOne(Type type, const DataLayout &dl) {
Type i32Ty = IntegerType::get(type.getContext(), ExtendBelowBits);
return ArgClassification::getExtend(i32Ty, /*signExt=*/intTy.isSigned());
}
- return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect();
}
if (auto indexTy = dyn_cast<IndexType>(type)) {
@@ -78,11 +78,11 @@ ArgClassification classifyOne(Type type, const DataLayout &dl) {
Type i32Ty = IntegerType::get(type.getContext(), ExtendBelowBits);
return ArgClassification::getExtend(i32Ty, /*signExt=*/true);
}
- return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect();
}
if (isa<FloatType, VectorType, MemRefType>(type))
- return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect();
// For dialect-specific types: query DataLayout via
// DataLayoutTypeInterface. Types that don't implement the interface
@@ -99,7 +99,7 @@ ArgClassification classifyOne(Type type, const DataLayout &dl) {
uint64_t sizeInBytes = (sizeInBits.getFixedValue() + 7) / 8;
if (sizeInBytes <= IndirectCutoffBytes)
- return ArgClassification::getDirect(/*offset=*/0);
+ return ArgClassification::getDirect();
uint64_t alignBytes = dl.getTypeABIAlignment(type);
return ArgClassification::getIndirect(llvm::Align(alignBytes),
diff --git a/mlir/unittests/ABI/ABIRewriteContextTest.cpp b/mlir/unittests/ABI/ABIRewriteContextTest.cpp
index e76c5f3821528..79c9124206562 100644
--- a/mlir/unittests/ABI/ABIRewriteContextTest.cpp
+++ b/mlir/unittests/ABI/ABIRewriteContextTest.cpp
@@ -36,7 +36,7 @@ TEST(ABIRewriteContextTest, MockCanBeConstructedAndDestroyed) {
}
TEST(ABIRewriteContextTest, ArgClassificationDirect) {
- auto c = ArgClassification::getDirect(/*offset=*/0);
+ auto c = ArgClassification::getDirect();
EXPECT_EQ(c.kind, ArgKind::Direct);
EXPECT_EQ(c.coercedType, nullptr);
EXPECT_TRUE(c.canFlatten);
@@ -84,8 +84,8 @@ TEST(ABIRewriteContextTest, ArgClassificationExtend) {
TEST(ABIRewriteContextTest, FunctionClassificationHoldsReturnAndArgs) {
FunctionClassification fc;
- fc.returnInfo = ArgClassification::getDirect(/*offset=*/0);
- fc.argInfos.push_back(ArgClassification::getDirect(/*offset=*/0));
+ fc.returnInfo = ArgClassification::getDirect();
+ fc.argInfos.push_back(ArgClassification::getDirect());
fc.argInfos.push_back(ArgClassification::getIndirect(llvm::Align(8), true));
fc.argInfos.push_back(ArgClassification::getIgnore());
>From 97c8facfea8283b4cd300674490b3a7b0d95a513 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 3 Sep 2026 08:45:15 -0700
Subject: [PATCH 6/8] [CIR][NFC] Use the CIR check prefix in the direct-offset
test
Assisted-by: Cursor / claude-opus-5
---
.../x86_64-struct-direct-offset.cir | 90 +++++++++----------
1 file changed, 45 insertions(+), 45 deletions(-)
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir
index 07132c8c308fc..585087886a09a 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-direct-offset.cir
@@ -1,4 +1,4 @@
-// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s --check-prefix=CIR
// 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
@@ -29,14 +29,14 @@ module attributes {
cir.return %2 : !s64i
}
- // CHECK-LABEL: cir.func{{.*}} @take_hiword(%arg0: !s64i) -> !s64i
- // CHECK: %[[SLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
- // CHECK: %[[U8:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
- // CHECK: %[[OFF:.*]] = cir.const #cir.int<8> : !s64i
- // CHECK: %[[GEP:.*]] = cir.ptr_stride %[[U8]], %[[OFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
- // CHECK: %[[CAST:.*]] = cir.cast bitcast %[[GEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
- // CHECK: cir.store %arg0, %[[CAST]] : !s64i, !cir.ptr<!s64i>
- // CHECK: %{{.*}} = cir.load %[[SLOT]] : !cir.ptr<!rec_HiWord>, !rec_HiWord
+ // CIR-LABEL: cir.func{{.*}} @take_hiword(%arg0: !s64i) -> !s64i
+ // CIR: %[[SLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CIR: %[[U8:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CIR: %[[OFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CIR: %[[GEP:.*]] = cir.ptr_stride %[[U8]], %[[OFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CIR: %[[CAST:.*]] = cir.cast bitcast %[[GEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CIR: cir.store %arg0, %[[CAST]] : !s64i, !cir.ptr<!s64i>
+ // CIR: %{{.*}} = cir.load %[[SLOT]] : !cir.ptr<!rec_HiWord>, !rec_HiWord
// The return value is likewise coerced from byte 8 of the local aggregate.
cir.func @give_hiword() -> !rec_HiWord {
@@ -45,15 +45,15 @@ module attributes {
cir.return %1 : !rec_HiWord
}
- // CHECK-LABEL: cir.func{{.*}} @give_hiword() -> !s64i
- // CHECK: %[[RSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
- // CHECK: cir.store %{{.*}}, %[[RSLOT]] : !rec_HiWord, !cir.ptr<!rec_HiWord>
- // CHECK: %[[RU8:.*]] = cir.cast bitcast %[[RSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
- // CHECK: %[[ROFF:.*]] = cir.const #cir.int<8> : !s64i
- // CHECK: %[[RGEP:.*]] = cir.ptr_stride %[[RU8]], %[[ROFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
- // CHECK: %[[RCAST:.*]] = cir.cast bitcast %[[RGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
- // CHECK: %[[RVAL:.*]] = cir.load %[[RCAST]] : !cir.ptr<!s64i>, !s64i
- // CHECK: cir.return %[[RVAL]] : !s64i
+ // CIR-LABEL: cir.func{{.*}} @give_hiword() -> !s64i
+ // CIR: %[[RSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CIR: cir.store %{{.*}}, %[[RSLOT]] : !rec_HiWord, !cir.ptr<!rec_HiWord>
+ // CIR: %[[RU8:.*]] = cir.cast bitcast %[[RSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CIR: %[[ROFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CIR: %[[RGEP:.*]] = cir.ptr_stride %[[RU8]], %[[ROFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CIR: %[[RCAST:.*]] = cir.cast bitcast %[[RGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CIR: %[[RVAL:.*]] = cir.load %[[RCAST]] : !cir.ptr<!s64i>, !s64i
+ // CIR: cir.return %[[RVAL]] : !s64i
// The caller reconstructs the aggregate from the callee's i64 result, so
// the received register is written back to byte 8 of a slot.
@@ -66,15 +66,15 @@ module attributes {
cir.return %3 : !s64i
}
- // CHECK-LABEL: cir.func{{.*}} @call_hiword() -> !s64i
- // CHECK: %[[CSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
- // CHECK: %[[CRET:.*]] = cir.call @give_hiword() : () -> !s64i
- // CHECK: %[[CU8:.*]] = cir.cast bitcast %[[CSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
- // CHECK: %[[COFF:.*]] = cir.const #cir.int<8> : !s64i
- // CHECK: %[[CGEP:.*]] = cir.ptr_stride %[[CU8]], %[[COFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
- // CHECK: %[[CCAST:.*]] = cir.cast bitcast %[[CGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
- // CHECK: cir.store %[[CRET]], %[[CCAST]] : !s64i, !cir.ptr<!s64i>
- // CHECK: %{{.*}} = cir.load %[[CSLOT]] : !cir.ptr<!rec_HiWord>, !rec_HiWord
+ // CIR-LABEL: cir.func{{.*}} @call_hiword() -> !s64i
+ // CIR: %[[CSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CIR: %[[CRET:.*]] = cir.call @give_hiword() : () -> !s64i
+ // CIR: %[[CU8:.*]] = cir.cast bitcast %[[CSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CIR: %[[COFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CIR: %[[CGEP:.*]] = cir.ptr_stride %[[CU8]], %[[COFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CIR: %[[CCAST:.*]] = cir.cast bitcast %[[CGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CIR: cir.store %[[CRET]], %[[CCAST]] : !s64i, !cir.ptr<!s64i>
+ // CIR: %{{.*}} = cir.load %[[CSLOT]] : !cir.ptr<!rec_HiWord>, !rec_HiWord
// Passing an aggregate argument runs the extraction direction at the call
// site: the operand's byte 8 is read out to form the register.
@@ -83,16 +83,16 @@ module attributes {
cir.return
}
- // CHECK-LABEL: cir.func{{.*}} @pass_hiword(%arg0: !s64i)
- // CHECK: %[[PREC:.*]] = cir.load %{{.*}} : !cir.ptr<!rec_HiWord>, !rec_HiWord
- // CHECK: %[[PSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
- // CHECK: cir.store %[[PREC]], %[[PSLOT]] : !rec_HiWord, !cir.ptr<!rec_HiWord>
- // CHECK: %[[PU8:.*]] = cir.cast bitcast %[[PSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
- // CHECK: %[[POFF:.*]] = cir.const #cir.int<8> : !s64i
- // CHECK: %[[PGEP:.*]] = cir.ptr_stride %[[PU8]], %[[POFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
- // CHECK: %[[PCAST:.*]] = cir.cast bitcast %[[PGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
- // CHECK: %[[PVAL:.*]] = cir.load %[[PCAST]] : !cir.ptr<!s64i>, !s64i
- // CHECK: %{{.*}} = cir.call @take_hiword(%[[PVAL]]) : (!s64i) -> !s64i
+ // CIR-LABEL: cir.func{{.*}} @pass_hiword(%arg0: !s64i)
+ // CIR: %[[PREC:.*]] = cir.load %{{.*}} : !cir.ptr<!rec_HiWord>, !rec_HiWord
+ // CIR: %[[PSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_HiWord>
+ // CIR: cir.store %[[PREC]], %[[PSLOT]] : !rec_HiWord, !cir.ptr<!rec_HiWord>
+ // CIR: %[[PU8:.*]] = cir.cast bitcast %[[PSLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+ // CIR: %[[POFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CIR: %[[PGEP:.*]] = cir.ptr_stride %[[PU8]], %[[POFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CIR: %[[PCAST:.*]] = cir.cast bitcast %[[PGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+ // CIR: %[[PVAL:.*]] = cir.load %[[PCAST]] : !cir.ptr<!s64i>, !s64i
+ // CIR: %{{.*}} = cir.call @take_hiword(%[[PVAL]]) : (!s64i) -> !s64i
// The same offset-8 coercion from a leading pad field rather than a nested
// empty record. Only 4 live bytes in the high eightbyte, so i32 not i64.
@@ -104,14 +104,14 @@ module attributes {
cir.return %2 : !s32i
}
- // CHECK-LABEL: cir.func{{.*}} @take_lead_pad(%arg0: !s32i) -> !s32i
- // CHECK: %[[LSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_LeadPad>
- // CHECK: %[[LU8:.*]] = cir.cast bitcast %[[LSLOT]] : !cir.ptr<!rec_LeadPad> -> !cir.ptr<!u8i>
- // CHECK: %[[LOFF:.*]] = cir.const #cir.int<8> : !s64i
- // CHECK: %[[LGEP:.*]] = cir.ptr_stride %[[LU8]], %[[LOFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
- // CHECK: %[[LCAST:.*]] = cir.cast bitcast %[[LGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s32i>
- // CHECK: cir.store %arg0, %[[LCAST]] : !s32i, !cir.ptr<!s32i>
- // CHECK: %{{.*}} = cir.load %[[LSLOT]] : !cir.ptr<!rec_LeadPad>, !rec_LeadPad
+ // CIR-LABEL: cir.func{{.*}} @take_lead_pad(%arg0: !s32i) -> !s32i
+ // CIR: %[[LSLOT:.*]] = cir.alloca "coerce" {{.*}} : !cir.ptr<!rec_LeadPad>
+ // CIR: %[[LU8:.*]] = cir.cast bitcast %[[LSLOT]] : !cir.ptr<!rec_LeadPad> -> !cir.ptr<!u8i>
+ // CIR: %[[LOFF:.*]] = cir.const #cir.int<8> : !s64i
+ // CIR: %[[LGEP:.*]] = cir.ptr_stride %[[LU8]], %[[LOFF]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
+ // CIR: %[[LCAST:.*]] = cir.cast bitcast %[[LGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s32i>
+ // CIR: cir.store %arg0, %[[LCAST]] : !s32i, !cir.ptr<!s32i>
+ // CIR: %{{.*}} = cir.load %[[LSLOT]] : !cir.ptr<!rec_LeadPad>, !rec_LeadPad
}
// LLVM: define i64 @take_hiword(i64 %{{.+}})
>From 360ab50c17d1fd262f5a3f86a209d4f33a9f4c22 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Tue, 8 Sep 2026 15:31:18 -0700
Subject: [PATCH 7/8] [CIR][NFC] Tighten the direct-offset test checks
The checks pinned the byte-8 literal and little else, and the CIR side only
checked signatures. They now chain the argument through the stride into the
store, on the callee and caller side both.
Assisted-by: Cursor / claude-opus-5
---
.../call-conv-lowering-x86_64-empty.cpp | 33 ++++++++++++++++---
.../test/CIR/CodeGenCXX/x86_64-arguments.cpp | 26 +++++++++++----
2 files changed, 48 insertions(+), 11 deletions(-)
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
index ceafb23baf14c..2cc73310c152e 100644
--- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
@@ -153,9 +153,16 @@ float takeFloatEmptyFirst(FloatEmptyFirst v) { return v.a; }
long takeHiWord(HiWord v) { return v.hi; }
// CIR: cir.func {{.*}}@_Z10takeHiWord6HiWord(%arg0: !s64i {{.*}}) -> (!s64i
-// LLVM: define dso_local noundef i64 @_Z10takeHiWord6HiWord(i64 %{{[^,]+}})
-// LLVM: %{{.+}} = getelementptr{{( inbounds)?}} i8, ptr %{{.+}}, i64 8
-// LLVM: store i64 %{{.+}}, ptr %{{.+}}, align 8
+// CIR: %[[SLOT:.+]] = cir.alloca "coerce"
+// CIR: %[[U8:.+]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!rec_HiWord> -> !cir.ptr<!u8i>
+// CIR: %[[OFF:.+]] = cir.const #cir.int<8> : !s64i
+// CIR: %[[GEP:.+]] = cir.ptr_stride %[[U8]], %[[OFF]]
+// CIR: %[[HI:.+]] = cir.cast bitcast %[[GEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+// CIR: cir.store %arg0, %[[HI]] : !s64i, !cir.ptr<!s64i>
+// LLVM: define dso_local noundef i64 @_Z10takeHiWord6HiWord(i64 %[[ARG:[^)]+]])
+// LLVM: %[[SLOT:.+]] = alloca %struct.HiWord, align 8
+// LLVM: %[[HI:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
+// LLVM: store i64 %[[ARG]], ptr %[[HI]], align 8
// The same offset on the return side.
HiWord giveHiWord(long hi) {
@@ -170,13 +177,29 @@ HiWord giveHiWord(long hi) {
// LLVM: %[[RVAL:.+]] = load i64, ptr %[[RGEP]], align 8
// LLVM: ret i64 %[[RVAL]]
-// The caller's own signature is unaffected by the callee's coercion.
+// Caller-side coercion, on the return received and the argument passed.
long callerHiWord(long hi) {
return takeHiWord(giveHiWord(hi));
}
// CIR: cir.func {{.*}}@_Z12callerHiWordl(%arg0: !s64i {{.*}}) -> (!s64i
-// LLVM: define dso_local noundef i64 @_Z12callerHiWordl(i64 noundef %{{[^,]+}})
+// CIR: %[[RET:.+]] = cir.call @_Z10giveHiWordl(
+// CIR: %[[ROFF:.+]] = cir.const #cir.int<8> : !s64i
+// CIR: %[[RGEP:.+]] = cir.ptr_stride %{{.+}}, %[[ROFF]]
+// CIR: %[[RPTR:.+]] = cir.cast bitcast %[[RGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+// CIR: cir.store %[[RET]], %[[RPTR]] : !s64i, !cir.ptr<!s64i>
+// CIR: %[[AOFF:.+]] = cir.const #cir.int<8> : !s64i
+// CIR: %[[AGEP:.+]] = cir.ptr_stride %{{.+}}, %[[AOFF]]
+// CIR: %[[APTR:.+]] = cir.cast bitcast %[[AGEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+// CIR: %[[AVAL:.+]] = cir.load %[[APTR]] : !cir.ptr<!s64i>, !s64i
+// CIR: %{{.+}} = cir.call @_Z10takeHiWord6HiWord(%[[AVAL]])
+// LLVM: define dso_local noundef i64 @_Z12callerHiWordl(i64 noundef %{{[^,)]+}})
+// LLVM: %[[RET:.+]] = call i64 @_Z10giveHiWordl(i64 noundef %{{.+}})
+// LLVM: %[[RSLOT:.+]] = getelementptr{{( inbounds)?}} i8, ptr %{{.+}}, i64 8
+// LLVM: store i64 %[[RET]], ptr %[[RSLOT]], align 8
+// LLVM: %[[ASLOT:.+]] = getelementptr{{( inbounds)?}} i8, ptr %{{.+}}, i64 8
+// LLVM: %[[AVAL:.+]] = load i64, ptr %[[ASLOT]], align 8
+// LLVM: %{{.+}} = call noundef i64 @_Z10takeHiWord6HiWord(i64 %[[AVAL]])
// Past two eightbytes SysV says memory whatever the content, so an empty class
// this size is passed indirectly at its declared alignment.
diff --git a/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp b/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp
index acd6eb6a5264c..0e934998f5372 100644
--- a/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp
+++ b/clang/test/CIR/CodeGenCXX/x86_64-arguments.cpp
@@ -43,11 +43,18 @@ void f3(struct s3_1 x) {}
// The empty member owns eightbyte 0, so the live long is the second eightbyte
// and the coercion starts at byte 8.
-// CIR-LABEL: cir.func {{.*}} @_Z2f34s3_1
-// LLVM-LABEL: define {{.*}} void @_Z2f34s3_1(i64 %{{[^,)]+}})
+// CIR-LABEL: cir.func {{.*}} @_Z2f34s3_1(%arg0: !s64i
+// CIR: %[[SLOT:.+]] = cir.alloca "coerce"
+// CIR: %[[U8:.+]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!rec_s3_1> -> !cir.ptr<!u8i>
+// CIR: %[[OFF:.+]] = cir.const #cir.int<8> : !s64i
+// CIR: %[[GEP:.+]] = cir.ptr_stride %[[U8]], %[[OFF]]
+// CIR: %[[HI:.+]] = cir.cast bitcast %[[GEP]] : !cir.ptr<!u8i> -> !cir.ptr<!s64i>
+// CIR: cir.store %arg0, %[[HI]] : !s64i, !cir.ptr<!s64i>
+// LLVM-LABEL: define {{.*}} void @_Z2f34s3_1(
+// LLVM-SAME: i64 %[[ARG:[^,)]+]])
// LLVM: %[[SLOT:.+]] = alloca %struct.s3_1, align 8
// LLVM: %[[HI:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
-// LLVM: store i64 %{{.+}}, ptr %[[HI]], align 8
+// LLVM: store i64 %[[ARG]], ptr %[[HI]], align 8
// OGCG-LABEL: define {{.*}} void @_Z2f34s3_1(i64 %x.coerce)
// OGCG: %[[SLOT:.+]] = alloca %struct.s3_1, align 8
// OGCG: %[[HI:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
@@ -127,11 +134,18 @@ namespace PR5179 {
}
// The empty base owns eightbyte 0, so the pointer is read from byte 8.
-// CIR-LABEL: cir.func {{.*}} @_ZN6PR51793barENS_2B2E
-// LLVM-LABEL: define {{.*}} ptr @_ZN6PR51793barENS_2B2E(ptr %{{[^,)]+}})
+// CIR-LABEL: cir.func {{.*}} @_ZN6PR51793barENS_2B2E(%arg0: !cir.ptr<!void>
+// CIR: %[[SLOT:.+]] = cir.alloca "coerce"
+// CIR: %[[U8:.+]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!rec_PR51793A3AB2> -> !cir.ptr<!u8i>
+// CIR: %[[OFF:.+]] = cir.const #cir.int<8> : !s64i
+// CIR: %[[GEP:.+]] = cir.ptr_stride %[[U8]], %[[OFF]]
+// CIR: %[[PA:.+]] = cir.cast bitcast %[[GEP]] : !cir.ptr<!u8i> -> !cir.ptr<!cir.ptr<!void>>
+// CIR: cir.store %arg0, %[[PA]] : !cir.ptr<!void>, !cir.ptr<!cir.ptr<!void>>
+// LLVM-LABEL: define {{.*}} ptr @_ZN6PR51793barENS_2B2E(
+// LLVM-SAME: ptr %[[ARG:[^,)]+]])
// LLVM: %[[SLOT:.+]] = alloca %"struct.PR5179::B2", align 8
// LLVM: %[[PA:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
-// LLVM: store ptr %{{.+}}, ptr %[[PA]], align 8
+// LLVM: store ptr %[[ARG]], ptr %[[PA]], align 8
// OGCG-LABEL: define {{.*}} ptr @_ZN6PR51793barENS_2B2E(ptr %b2.coerce)
// OGCG: %[[SLOT:.+]] = alloca %"struct.PR5179::B2", align 8
// OGCG: %[[PA:.+]] = getelementptr{{( inbounds)?}} i8, ptr %[[SLOT]], i64 8
>From 8fb811dc6ff9f2cf37d10b12e6ed3d3f31af95e1 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 9 Sep 2026 07:01:27 -0700
Subject: [PATCH 8/8] [CIR][NFC] Order getDirect arguments like ABIArgInfo
Assisted-by: Cursor / claude-opus-5
---
clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp | 2 +-
mlir/include/mlir/ABI/ABIRewriteContext.h | 4 ++--
mlir/lib/ABI/Targets/Test/TestTarget.cpp | 2 +-
mlir/unittests/ABI/ABIRewriteContextTest.cpp | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index d0d8479ae89ab..4464d41fd3d2d 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -550,7 +550,7 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
// trip for nothing.
if (comparesAgainstCoerce && coerced == origTy)
return ArgClassification::getDirect();
- return ArgClassification::getDirect(offset, coerced);
+ return ArgClassification::getDirect(coerced, offset);
}
// An extended value is always read from byte 0 of its own storage, so
// there is no offset to honor here.
diff --git a/mlir/include/mlir/ABI/ABIRewriteContext.h b/mlir/include/mlir/ABI/ABIRewriteContext.h
index 430ebe94bc5b6..93cbad44a224d 100644
--- a/mlir/include/mlir/ABI/ABIRewriteContext.h
+++ b/mlir/include/mlir/ABI/ABIRewriteContext.h
@@ -98,10 +98,10 @@ struct ArgClassification {
}
static ArgClassification getDirect() {
- return getDirect(/*offset=*/0, /*coerced=*/nullptr);
+ return getDirect(/*coerced=*/nullptr, /*offset=*/0);
}
- static ArgClassification getDirect(unsigned offset, Type coerced) {
+ static ArgClassification getDirect(Type coerced, unsigned offset) {
// isPassThrough reads only coercedType, so an offset with no coerced
// type to read at it would be silently ignored.
assert((!offset || coerced) &&
diff --git a/mlir/lib/ABI/Targets/Test/TestTarget.cpp b/mlir/lib/ABI/Targets/Test/TestTarget.cpp
index f36595ae52bb4..19bcb83b63d5e 100644
--- a/mlir/lib/ABI/Targets/Test/TestTarget.cpp
+++ b/mlir/lib/ABI/Targets/Test/TestTarget.cpp
@@ -161,7 +161,7 @@ parseOne(DictionaryAttr argDict, function_ref<InFlightDiagnostic()> emitError) {
Type coerced;
if (auto t = argDict.getAs<TypeAttr>("coerced_type"))
coerced = t.getValue();
- auto c = ArgClassification::getDirect(/*offset=*/0, coerced);
+ auto c = ArgClassification::getDirect(coerced, /*offset=*/0);
if (auto cf = argDict.getAs<BoolAttr>("can_flatten"))
c.canFlatten = cf.getValue();
return c;
diff --git a/mlir/unittests/ABI/ABIRewriteContextTest.cpp b/mlir/unittests/ABI/ABIRewriteContextTest.cpp
index 79c9124206562..789257d318685 100644
--- a/mlir/unittests/ABI/ABIRewriteContextTest.cpp
+++ b/mlir/unittests/ABI/ABIRewriteContextTest.cpp
@@ -45,7 +45,7 @@ TEST(ABIRewriteContextTest, ArgClassificationDirect) {
TEST(ABIRewriteContextTest, ArgClassificationDirectWithType) {
MLIRContext mlirCtx;
auto i32 = IntegerType::get(&mlirCtx, 32);
- auto c = ArgClassification::getDirect(/*offset=*/0, i32);
+ auto c = ArgClassification::getDirect(i32, /*offset=*/0);
EXPECT_EQ(c.kind, ArgKind::Direct);
EXPECT_EQ(c.coercedType, i32);
}
More information about the cfe-commits
mailing list