[clang] 1fe5589 - [CIR] Accept _BitInt up to 128 bits in x86_64 callconv lowering (#212668)

via cfe-commits cfe-commits at lists.llvm.org
Fri Jul 31 09:20:48 PDT 2026


Author: Adam Smith
Date: 2026-07-31T11:20:43-05:00
New Revision: 1fe55891a7fcaac526a2b224432145e80f79dc52

URL: https://github.com/llvm/llvm-project/commit/1fe55891a7fcaac526a2b224432145e80f79dc52
DIFF: https://github.com/llvm/llvm-project/commit/1fe55891a7fcaac526a2b224432145e80f79dc52.diff

LOG: [CIR] Accept _BitInt up to 128 bits in x86_64 callconv lowering (#212668)

The x86_64 bridge in `CallConvLoweringPass.cpp` rejected every
`_BitInt`, so a function taking or returning one reported NYI even
though the ABI library already classifies these types. The classifier
has to be told the integer is bit-precise, and `mapCIRType` was dropping
that flag, which its two-eightbyte handling keys on. With the flag
forwarded, accepting widths up to 128 is mostly a matter of letting the
classifier's answer through. Wider widths stay rejected, for the reason
on the accept check.

Relaxing that check alone was not enough. `convertABIArgInfo` discarded
the classifier's coerce for every non-aggregate, which would have left a
`_BitInt(33)` as i33 where classic CodeGen passes i64. It now takes the
coercion path for a multi-register tuple coerce and for one wider than
the natural type. A narrower or equal coerce still means unchanged,
which is what preserves a `_BitInt(128)`'s 8-byte alignment.

Two `noundef` gaps against classic CodeGen remain. Neither mechanism is
new, but this change is what makes both reachable for `_BitInt`. CIRGen
omits the attribute for a width that is not a multiple of 8, and the
rewriter attaches none to the arguments it creates when flattening, so a
`_BitInt(72)` that CIRGen did mark loses it once the pair is split.

Added: 
    clang/test/CIR/Transforms/abi-lowering/x86_64-bitint.cir

Modified: 
    clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
    clang/test/CIR/Transforms/abi-lowering/x86_64-int-nyi.cir

Removed: 
    


################################################################################
diff  --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index f3e42ac5e73b7..06dae4c541e39 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -67,11 +67,11 @@ namespace {
 // Maps CIR types to llvm::abi::Type, runs the LLVM ABI Lowering Library's
 // SysV x86_64 classifier, and converts the result back into the
 // dialect-agnostic mlir::abi::FunctionClassification that CIRABIRewriteContext
-// consumes.  Integer / pointer / bool / f32 / f64 scalars and struct / array
-// aggregates are handled; unions, `_BitInt`, `_Complex`, vectors, wider
-// floats, and packed or padded records are reported NYI by
-// classifyX86_64Function so an unsupported signature fails the pass instead of
-// being misclassified.
+// consumes.  Integer (including `_BitInt` up to 128 bits) / pointer / bool /
+// f32 / f64 scalars and struct / array aggregates are handled.  Unions,
+// `_Complex`, vectors, wider floats, and packed or padded records are reported
+// NYI by classifyX86_64Function so an unsupported signature fails the pass
+// instead of being misclassified.
 //===----------------------------------------------------------------------===//
 
 /// Whether a struct's declared argument-passing kind (from the module's
@@ -92,12 +92,13 @@ static bool recordCanPassInRegs(ModuleOp modOp, cir::RecordType recTy) {
   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 packed or padded records are not handled and are reported NYI
-/// at the reject() choke point in classifyX86_64Function.
+/// The CIR types the x86_64 bridge handles.  Scalars: an integer up to 128
+/// bits (including `_BitInt` and `__int128`), pointer, bool, void, f32, or f64.
+/// Aggregates: a complete struct whose fields are all themselves supported, or
+/// an array of a supported element type.  A `_BitInt` wider than 128 bits,
+/// unions, `_Complex`, vectors, wider 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
@@ -108,14 +109,18 @@ static bool isSupportedType(mlir::Type ty) {
   if (isa<cir::VoidType, cir::BoolType, cir::SingleType, cir::DoubleType>(ty))
     return true;
   if (auto intTy = dyn_cast<cir::IntType>(ty)) {
-    // Integers up to 64 bits and __int128 are Direct; convertABIArgInfo's
-    // scalar branch passes them through (with sign/zero extension for
-    // sub-register widths), matching classic CodeGen.  Intermediate widths
-    // (65..127) do not arise from C and would need a coercion the Direct
-    // branch does not apply, so they stay rejected, as does _BitInt pending
-    // its coercion and padding handling.
+    // Integers up to 64 bits, __int128, and _BitInt up to 128 bits are
+    // handled: the classifier extends a width below 32, widens 33 through 63
+    // to i64, coerces 65 through 127 to a {i64, i64} pair, and passes 32, 64,
+    // and 128 in the natural type.  A wider _BitInt classifies Indirect,
+    // where at a multiple of 8 the byval attributes the rewriter appends
+    // duplicate the llvm.noundef CIRGen already emitted and trip the
+    // uniqueness assertion on the merged dictionary.  The bound is a blanket
+    // 128 because the widths that do not collide reach that same untested
+    // Indirect path.  Non-_BitInt intermediate widths (65..127) do not arise
+    // from C.  Both stay rejected.
     if (intTy.getIsBitInt())
-      return false;
+      return intTy.getWidth() <= 128;
     return intTy.getWidth() <= 64 || intTy.getWidth() == 128;
   }
   if (auto arrTy = dyn_cast<cir::ArrayType>(ty))
@@ -182,7 +187,7 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
       .Case([&](cir::IntType intTy) {
         return tb.getIntegerType(intTy.getWidth(),
                                  llvm::Align(dl.getTypeABIAlignment(type)),
-                                 intTy.isSigned());
+                                 intTy.isSigned(), intTy.getIsBitInt());
       })
       .Case([&](cir::PointerType ptrTy) {
         unsigned addrSpace = 0;
@@ -249,43 +254,60 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
 /// Convert an llvm::abi::ArgInfo into the ArgClassification consumed by
 /// CIRABIRewriteContext.
 ///
-/// 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.
+/// 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
+/// 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 (an SSE <2 x float>, say) 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.
-/// Every ArgInfo::getExtend() call site in the x86_64 classifier
-/// (llvm/lib/ABI/Targets/X86.cpp) is gated on the operand being an integer,
-/// so a non-integer, non-bool origTy here would mean the classifier
-/// disagreed with its own source -- asserted rather than silently handled.
+/// The x86_64 classifier (llvm/lib/ABI/Targets/X86.cpp) only returns Extend
+/// for an integer or bool operand, so any other origTy is asserted rather
+/// than silently handled.
 ///
 /// 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, and a zero-field
-/// (empty) record is dropped from the signature.
+/// Ignore: a void return, or a zero-field record dropped from the signature.
 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))
+    // 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.
+    // Leaving a scalar alone also preserves its ABI alignment: abiTypeToCIR
+    // drops the bit-precise flag, so a _BitInt(128) routed through it would
+    // come back as !cir.int<s, 128> with __int128's 16-byte alignment instead
+    // of 8.
+    const llvm::abi::Type *coerceAbi = info.getCoerceToType();
+    bool isAggregate = isa_and_present<cir::RecordType, cir::ArrayType>(origTy);
+    bool coerceIsRegisterTuple =
+        isa_and_present<llvm::abi::RecordType>(coerceAbi);
+    // 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);
+    const auto *coerceInt =
+        dyn_cast_if_present<llvm::abi::IntegerType>(coerceAbi);
+    bool coerceWidensScalar =
+        origInt && coerceInt &&
+        coerceInt->getSizeInBits().getFixedValue() > origInt.getWidth();
+    if (!isAggregate && !coerceIsRegisterTuple && !coerceWidensScalar)
       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);
+    // The coerce must be a type this bridge can represent.  One it 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 value as an unchanged by-value
+    // record.
+    mlir::Type coerced = abiTypeToCIR(coerceAbi, ctx);
     if (!coerced)
       return std::nullopt;
     return ArgClassification::getDirect(coerced);
   }
   if (info.isExtend()) {
-    if (origTy && isa<cir::BoolType>(origTy))
+    if (isa_and_present<cir::BoolType>(origTy))
       return ArgClassification::getExtend(nullptr, info.isSignExt());
     assert((!origTy || isa<cir::IntType>(origTy)) &&
            "the x86_64 classifier only returns Extend for integers and bool");

diff  --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-bitint.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-bitint.cir
new file mode 100644
index 0000000000000..13c81882e9d0b
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-bitint.cir
@@ -0,0 +1,815 @@
+// 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
+
+!b17i = !cir.int<s, 17, bitint>
+!ub17i = !cir.int<u, 17, bitint>
+!b32i = !cir.int<s, 32, bitint>
+!b33i = !cir.int<s, 33, bitint>
+!ub33i = !cir.int<u, 33, bitint>
+!b63i = !cir.int<s, 63, bitint>
+!b64i = !cir.int<s, 64, bitint>
+!b65i = !cir.int<s, 65, bitint>
+!ub65i = !cir.int<u, 65, bitint>
+!b127i = !cir.int<s, 127, bitint>
+!b128i = !cir.int<s, 128, bitint>
+!s8i = !cir.int<s, 8>
+!rec_S = !cir.struct<"S" {!b17i}>
+!rec_P = !cir.struct<"P" {!b65i}>
+!rec_W = !cir.struct<"W" {!b128i}>
+!rec_O = !cir.struct<"O" {!b128i, !s8i}>
+!rec_Arr = !cir.struct<"Arr" {!cir.array<!b65i x 2>}>
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+    #dlti.dl_entry<i16, dense<16>: vector<2xi64>>,
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>,
+    #dlti.dl_entry<i128, dense<128>: vector<2xi64>>>
+} {
+
+  // A sub-register width is Extend: signext, and no coercion in the body.
+  cir.func @ret_b17(%arg0: !b17i) -> !b17i {
+    cir.return %arg0 : !b17i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b17(
+  // CHECK-SAME:    %arg0: !cir.int<s, 17, bitint> {llvm.signext})
+  // CHECK-SAME:    -> (!cir.int<s, 17, bitint> {llvm.signext})
+  // CHECK-NEXT:    cir.return %arg0 : !cir.int<s, 17, bitint>
+
+  // LLVM-LABEL: define signext i17 @ret_b17(
+  // LLVM-SAME:    i17 signext %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    ret i17 %[[ARG]]
+
+  // Unsigned takes zeroext instead.
+  cir.func @ret_ub17(%arg0: !ub17i) -> !ub17i {
+    cir.return %arg0 : !ub17i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_ub17(
+  // CHECK-SAME:    %arg0: !cir.int<u, 17, bitint> {llvm.zeroext})
+  // CHECK-SAME:    -> (!cir.int<u, 17, bitint> {llvm.zeroext})
+  // CHECK-NEXT:    cir.return %arg0 : !cir.int<u, 17, bitint>
+
+  // LLVM-LABEL: define zeroext i17 @ret_ub17(
+  // LLVM-SAME:    i17 zeroext %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    ret i17 %[[ARG]]
+
+  // Already the width the classifier names: Direct, nothing to rewrite.
+  cir.func @ret_b32(%arg0: !b32i) -> !b32i {
+    cir.return %arg0 : !b32i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b32(%arg0: !s32i_bitint) -> !s32i_bitint
+  // CHECK-NEXT:    cir.return %arg0 : !s32i_bitint
+
+  // LLVM-LABEL: define i32 @ret_b32(
+  // LLVM-SAME:    i32 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    ret i32 %[[ARG]]
+
+  // 33 bits travels in an i64.  Its five bytes are fewer than the eightbyte,
+  // so both coerce slots are typed i64 and the narrow views sit on the
+  // accesses.  That memory round trip is a truncate and a re-extend in LLVM.
+  cir.func @ret_b33(%arg0: !b33i) -> !b33i {
+    cir.return %arg0 : !b33i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b33(%arg0: !u64i) -> !u64i
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[ARGSLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGVIEW:[0-9]+]] = cir.cast bitcast %[[ARGSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[ARGVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 33, bitint>>, !cir.int<s, 33, bitint>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 33, bitint>, !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[RETSLOT]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    cir.return %[[WIDE]] : !u64i
+
+  // LLVM-LABEL: define i64 @ret_b33(
+  // LLVM-SAME:    i64 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    %[[RETSLOT:[0-9]+]] = alloca i64
+  // LLVM-NEXT:    %[[ARGSLOT:[0-9]+]] = alloca i64
+  // LLVM-NEXT:    store i64 %[[ARG]], ptr %[[ARGSLOT]]
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i64, ptr %[[ARGSLOT]]
+  // LLVM-NEXT:    %[[NARROW:[0-9]+]] = trunc i64 %[[WIDE]] to i33
+  // LLVM-NEXT:    %[[EXT:[0-9]+]] = sext i33 %[[NARROW]] to i64
+  // LLVM-NEXT:    store i64 %[[EXT]], ptr %[[RETSLOT]]
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load i64, ptr %[[RETSLOT]]
+  // LLVM-NEXT:    ret i64 %[[RES]]
+
+  // Signedness changes only the extension back into the eightbyte.
+  cir.func @ret_ub33(%arg0: !ub33i) -> !ub33i {
+    cir.return %arg0 : !ub33i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_ub33(%arg0: !u64i) -> !u64i
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[ARGSLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGVIEW:[0-9]+]] = cir.cast bitcast %[[ARGSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<u, 33, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[ARGVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<u, 33, bitint>>, !cir.int<u, 33, bitint>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<u, 33, bitint>>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.int<u, 33, bitint>, !cir.ptr<!cir.int<u, 33, bitint>>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[RETSLOT]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    cir.return %[[WIDE]] : !u64i
+
+  // LLVM-LABEL: define i64 @ret_ub33(
+  // LLVM-SAME:    i64 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    %[[RETSLOT:[0-9]+]] = alloca i64
+  // LLVM-NEXT:    %[[ARGSLOT:[0-9]+]] = alloca i64
+  // LLVM-NEXT:    store i64 %[[ARG]], ptr %[[ARGSLOT]]
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i64, ptr %[[ARGSLOT]]
+  // LLVM-NEXT:    %[[NARROW:[0-9]+]] = trunc i64 %[[WIDE]] to i33
+  // LLVM-NEXT:    %[[EXT:[0-9]+]] = zext i33 %[[NARROW]] to i64
+  // LLVM-NEXT:    store i64 %[[EXT]], ptr %[[RETSLOT]]
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load i64, ptr %[[RETSLOT]]
+  // LLVM-NEXT:    ret i64 %[[RES]]
+
+  // 63 bits fills the eightbyte, and the slot takes whichever type is larger,
+  // so here it is the bit-precise one and the i64 view moves to the load.
+  cir.func @ret_b63(%arg0: !b63i) -> !b63i {
+    cir.return %arg0 : !b63i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b63(%arg0: !u64i) -> !u64i
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 63, bitint>>
+  // CHECK-NEXT:    %[[ARGSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[ARGSLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGVIEW:[0-9]+]] = cir.cast bitcast %[[ARGSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 63, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[ARGVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 63, bitint>>, !cir.int<s, 63, bitint>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.int<s, 63, bitint>, !cir.ptr<!cir.int<s, 63, bitint>>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 63, bitint>> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[RETVIEW]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    cir.return %[[WIDE]] : !u64i
+
+  // LLVM-LABEL: define i64 @ret_b63(
+  // LLVM-SAME:    i64 %[[ARG:[0-9]+]])
+  // LLVM:         store i64 %[[ARG]], ptr %[[ARGSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i64, ptr %[[ARGSLOT]], align 8
+  // LLVM-NEXT:    %[[NARROW:[0-9]+]] = trunc i64 %[[WIDE]] to i63
+  // LLVM-NEXT:    %[[EXT:[0-9]+]] = sext i63 %[[NARROW]] to i64
+  // LLVM-NEXT:    store i64 %[[EXT]], ptr %[[RETSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load i64, ptr %[[RETSLOT]], align 8
+  // LLVM-NEXT:    ret i64 %[[RES]]
+
+  // A _BitInt exactly filling one register is Direct, signature unchanged.
+  cir.func @ret_b64(%arg0: !b64i) -> !b64i {
+    cir.return %arg0 : !b64i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b64(%arg0: !s64i_bitint) -> !s64i_bitint
+  // CHECK-NEXT:    cir.return %arg0 : !s64i_bitint
+
+  // LLVM-LABEL: define i64 @ret_b64(
+  // LLVM-SAME:    i64 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    ret i64 %[[ARG]]
+
+  // Two INTEGER eightbytes: coerced to a {i64, i64} pair the rewriter flattens
+  // into two arguments, which the prologue writes back into one slot.
+  cir.func @ret_b65(%arg0: !b65i) -> !b65i {
+    cir.return %arg0 : !b65i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b65(%arg0: !u64i, %arg1: !u64i)
+  // CHECK-SAME:    -> !rec_anon_struct
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[VALSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[E0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[E0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[E1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[E1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[PAIR]], %[[VALSLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[VALVIEW:[0-9]+]] = cir.cast bitcast %[[VALSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[VALVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 65, bitint>>, !cir.int<s, 65, bitint>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 65, bitint>, !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.return %[[WIDE]] : !rec_anon_struct
+
+  // LLVM-LABEL: define { i64, i64 } @ret_b65(
+  // LLVM-SAME:    i64 %[[A0:[0-9]+]], i64 %[[A1:[0-9]+]])
+  // LLVM-NEXT:    %[[RETSLOT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[VALSLOT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[FLAT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[E0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 0
+  // LLVM-NEXT:    store i64 %[[A0]], ptr %[[E0]]
+  // LLVM-NEXT:    %[[E1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 1
+  // LLVM-NEXT:    store i64 %[[A1]], ptr %[[E1]]
+  // LLVM-NEXT:    %[[PAIR:[0-9]+]] = load { i64, i64 }, ptr %[[FLAT]]
+  // LLVM-NEXT:    store { i64, i64 } %[[PAIR]], ptr %[[VALSLOT]]
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i128, ptr %[[VALSLOT]]
+  // LLVM-NEXT:    %[[NARROW:[0-9]+]] = trunc i128 %[[WIDE]] to i65
+  // LLVM-NEXT:    %[[EXT:[0-9]+]] = sext i65 %[[NARROW]] to i128
+  // LLVM-NEXT:    store i128 %[[EXT]], ptr %[[RETSLOT]]
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load { i64, i64 }, ptr %[[RETSLOT]]
+  // LLVM-NEXT:    ret { i64, i64 } %[[RES]]
+
+  // Unsigned pairs the same way and zero-extends on the way back.
+  cir.func @ret_ub65(%arg0: !ub65i) -> !ub65i {
+    cir.return %arg0 : !ub65i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_ub65(%arg0: !u64i, %arg1: !u64i)
+  // CHECK-SAME:    -> !rec_anon_struct
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[VALSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[E0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[E0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[E1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[E1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[PAIR]], %[[VALSLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[VALVIEW:[0-9]+]] = cir.cast bitcast %[[VALSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<u, 65, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[VALVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<u, 65, bitint>>, !cir.int<u, 65, bitint>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<u, 65, bitint>>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.int<u, 65, bitint>, !cir.ptr<!cir.int<u, 65, bitint>>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.return %[[WIDE]] : !rec_anon_struct
+
+  // LLVM-LABEL: define { i64, i64 } @ret_ub65(
+  // LLVM-SAME:    i64 %[[A0:[0-9]+]], i64 %[[A1:[0-9]+]])
+  // LLVM-NEXT:    %[[RETSLOT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[VALSLOT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[FLAT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[E0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 0
+  // LLVM-NEXT:    store i64 %[[A0]], ptr %[[E0]]
+  // LLVM-NEXT:    %[[E1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 1
+  // LLVM-NEXT:    store i64 %[[A1]], ptr %[[E1]]
+  // LLVM-NEXT:    %[[PAIR:[0-9]+]] = load { i64, i64 }, ptr %[[FLAT]]
+  // LLVM-NEXT:    store { i64, i64 } %[[PAIR]], ptr %[[VALSLOT]]
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i128, ptr %[[VALSLOT]]
+  // LLVM-NEXT:    %[[NARROW:[0-9]+]] = trunc i128 %[[WIDE]] to i65
+  // LLVM-NEXT:    %[[EXT:[0-9]+]] = zext i65 %[[NARROW]] to i128
+  // LLVM-NEXT:    store i128 %[[EXT]], ptr %[[RETSLOT]]
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load { i64, i64 }, ptr %[[RETSLOT]]
+  // LLVM-NEXT:    ret { i64, i64 } %[[RES]]
+
+  // 127 bits fills both eightbytes, so the slot is the bit-precise type again.
+  cir.func @ret_b127(%arg0: !b127i) -> !b127i {
+    cir.return %arg0 : !b127i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b127(%arg0: !u64i, %arg1: !u64i)
+  // CHECK-SAME:    -> !rec_anon_struct
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 127, bitint>>
+  // CHECK-NEXT:    %[[VALSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[E0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[E0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[E1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[E1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[PAIR]], %[[VALSLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[VALVIEW:[0-9]+]] = cir.cast bitcast %[[VALSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 127, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[VALVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 127, bitint>>, !cir.int<s, 127, bitint>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.int<s, 127, bitint>, !cir.ptr<!cir.int<s, 127, bitint>>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 127, bitint>> -> !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.return %[[WIDE]] : !rec_anon_struct
+
+  // LLVM-LABEL: define { i64, i64 } @ret_b127(
+  // LLVM-SAME:    i64 %[[A0:[0-9]+]], i64 %[[A1:[0-9]+]])
+  // LLVM-NEXT:    %[[RETSLOT:[0-9]+]] = alloca i128
+  // LLVM-NEXT:    %[[VALSLOT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[FLAT:[0-9]+]] = alloca { i64, i64 }
+  // LLVM-NEXT:    %[[E0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 0
+  // LLVM-NEXT:    store i64 %[[A0]], ptr %[[E0]]
+  // LLVM-NEXT:    %[[E1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 1
+  // LLVM-NEXT:    store i64 %[[A1]], ptr %[[E1]]
+  // LLVM-NEXT:    %[[PAIR:[0-9]+]] = load { i64, i64 }, ptr %[[FLAT]]
+  // LLVM-NEXT:    store { i64, i64 } %[[PAIR]], ptr %[[VALSLOT]]
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i128, ptr %[[VALSLOT]]
+  // LLVM-NEXT:    %[[NARROW:[0-9]+]] = trunc i128 %[[WIDE]] to i127
+  // LLVM-NEXT:    %[[EXT:[0-9]+]] = sext i127 %[[NARROW]] to i128
+  // LLVM-NEXT:    store i128 %[[EXT]], ptr %[[RETSLOT]]
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load { i64, i64 }, ptr %[[RETSLOT]]
+  // LLVM-NEXT:    ret { i64, i64 } %[[RES]]
+
+  // Exactly two eightbytes: passed as one i128, nothing to rewrite.
+  cir.func @ret_b128(%arg0: !b128i) -> !b128i {
+    cir.return %arg0 : !b128i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_b128(%arg0: !s128i_bitint) -> !s128i_bitint
+  // CHECK-NEXT:    cir.return %arg0 : !s128i_bitint
+
+  // LLVM-LABEL: define i128 @ret_b128(
+  // LLVM-SAME:    i128 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    ret i128 %[[ARG]]
+
+  // A call site widens the argument and narrows the result symmetrically, so
+  // the truncate and extend appear on both sides of the call.
+  cir.func @call_b33(%arg0: !b33i) -> !b33i {
+    %0 = cir.call @ret_b33(%arg0) : (!b33i) -> !b33i
+    cir.return %0 : !b33i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @call_b33(%arg0: !u64i) -> !u64i
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[INSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[INSLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[INVIEW:[0-9]+]] = cir.cast bitcast %[[INSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[INVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 33, bitint>>, !cir.int<s, 33, bitint>
+  // CHECK-NEXT:    %[[RESSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGSLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[ARGVIEW:[0-9]+]] = cir.cast bitcast %[[ARGSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[ARGVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 33, bitint>, !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[WIDE:[0-9]+]] = cir.load %[[ARGSLOT]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    %[[RES:[0-9]+]] = cir.call @ret_b33(%[[WIDE]]) : (!u64i) -> !u64i
+  // CHECK-NEXT:    cir.store %[[RES]], %[[RESSLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[RESVIEW:[0-9]+]] = cir.cast bitcast %[[RESSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[NARROW:[0-9]+]] = cir.load %[[RESVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 33, bitint>>, !cir.int<s, 33, bitint>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    cir.store %[[NARROW]], %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 33, bitint>, !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[OUT:[0-9]+]] = cir.load %[[RETSLOT]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    cir.return %[[OUT]] : !u64i
+
+  // LLVM-LABEL: define i64 @call_b33(
+  // LLVM-SAME:    i64 %[[ARG:[0-9]+]])
+  // LLVM:         store i64 %[[ARG]], ptr %[[INSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[INWIDE:[0-9]+]] = load i64, ptr %[[INSLOT]], align 8
+  // LLVM-NEXT:    %[[VAL:[0-9]+]] = trunc i64 %[[INWIDE]] to i33
+  // LLVM:         %[[ARGEXT:[0-9]+]] = sext i33 %[[VAL]] to i64
+  // LLVM-NEXT:    store i64 %[[ARGEXT]], ptr %[[ARGSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i64, ptr %[[ARGSLOT]], align 8
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = call i64 @ret_b33(i64 %[[WIDE]])
+  // LLVM-NEXT:    store i64 %[[RES]], ptr %[[RESSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[RESWIDE:[0-9]+]] = load i64, ptr %[[RESSLOT]], align 8
+  // LLVM-NEXT:    %[[RESNARROW:[0-9]+]] = trunc i64 %[[RESWIDE]] to i33
+  // LLVM-NEXT:    %[[RETEXT:[0-9]+]] = sext i33 %[[RESNARROW]] to i64
+  // LLVM-NEXT:    store i64 %[[RETEXT]], ptr %[[RETSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[OUT:[0-9]+]] = load i64, ptr %[[RETSLOT]], align 8
+  // LLVM-NEXT:    ret i64 %[[OUT]]
+
+  // A call site decomposes the argument into the two registers the callee
+  // expects and reassembles the returned pair.
+  cir.func @call_b65(%arg0: !b65i) -> !b65i {
+    %0 = cir.call @ret_b65(%arg0) : (!b65i) -> !b65i
+    cir.return %0 : !b65i
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @call_b65(%arg0: !u64i, %arg1: !u64i)
+  // CHECK-SAME:    -> !rec_anon_struct
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[INSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[I0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[I0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[I1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[I1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[INPAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[INPAIR]], %[[INSLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[INVIEW:[0-9]+]] = cir.cast bitcast %[[INSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[INVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 65, bitint>>, !cir.int<s, 65, bitint>
+  // CHECK-NEXT:    %[[RESSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[ARGSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[ARGVIEW:[0-9]+]] = cir.cast bitcast %[[ARGSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[ARGVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 65, bitint>, !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[A0P:[0-9]+]] = cir.get_member %[[ARGSLOT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[A0:[0-9]+]] = cir.load %[[A0P]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    %[[A1P:[0-9]+]] = cir.get_member %[[ARGSLOT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[A1:[0-9]+]] = cir.load %[[A1P]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    %[[RES:[0-9]+]] = cir.call @ret_b65(%[[A0]], %[[A1]])
+  // CHECK-SAME:      : (!u64i, !u64i) -> !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[RES]], %[[RESSLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[RESVIEW:[0-9]+]] = cir.cast bitcast %[[RESSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[NARROW:[0-9]+]] = cir.load %[[RESVIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 65, bitint>>, !cir.int<s, 65, bitint>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    cir.store %[[NARROW]], %[[RETVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 65, bitint>, !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[OUT:[0-9]+]] = cir.load %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.return %[[OUT]] : !rec_anon_struct
+
+  // LLVM-LABEL: define { i64, i64 } @call_b65(
+  // LLVM-SAME:    i64 %{{[0-9]+}}, i64 %{{[0-9]+}})
+  // LLVM:         %[[VAL:[0-9]+]] = trunc i128 %{{[0-9]+}} to i65
+  // LLVM:         %[[EXT:[0-9]+]] = sext i65 %[[VAL]] to i128
+  // LLVM-NEXT:    store i128 %[[EXT]], ptr %[[ARGSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[A0P:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[ARGSLOT]], i32 0, i32 0
+  // LLVM-NEXT:    %[[A0:[0-9]+]] = load i64, ptr %[[A0P]]
+  // LLVM-NEXT:    %[[A1P:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[ARGSLOT]], i32 0, i32 1
+  // LLVM-NEXT:    %[[A1:[0-9]+]] = load i64, ptr %[[A1P]]
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = call { i64, i64 } @ret_b65(i64 %[[A0]], i64 %[[A1]])
+  // LLVM-NEXT:    store { i64, i64 } %[[RES]], ptr %[[RESSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[RESWIDE:[0-9]+]] = load i128, ptr %[[RESSLOT]]
+  // LLVM-NEXT:    %[[RESNARROW:[0-9]+]] = trunc i128 %[[RESWIDE]] to i65
+  // LLVM-NEXT:    %[[RETEXT:[0-9]+]] = sext i65 %[[RESNARROW]] to i128
+  // LLVM-NEXT:    store i128 %[[RETEXT]], ptr %[[RETSLOT:[0-9]+]], align 8
+  // LLVM-NEXT:    %[[OUT:[0-9]+]] = load { i64, i64 }, ptr %[[RETSLOT]], align 8
+  // LLVM-NEXT:    ret { i64, i64 } %[[OUT]]
+
+  // Widths that classify 
diff erently in one signature: the 65-bit parameter
+  // expands to two registers, shifting the 33-bit one to the fourth position
+  // while the extended one stays where it was.
+  cir.func @take_widths(%arg0: !b17i, %arg1: !b65i, %arg2: !b33i) {
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @take_widths(
+  // CHECK-SAME:    %arg0: !cir.int<s, 17, bitint> {llvm.signext}
+  // CHECK-SAME:    , %arg1: !u64i, %arg2: !u64i, %arg3: !u64i)
+  // CHECK-NEXT:    %[[W33SLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg3, %[[W33SLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[W33VIEW:[0-9]+]] = cir.cast bitcast %[[W33SLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %{{[0-9]+}} = cir.load %[[W33VIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 33, bitint>>, !cir.int<s, 33, bitint>
+  // CHECK-NEXT:    %[[P65SLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[E0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[E0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[E1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg2, %[[E1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[PAIR]], %[[P65SLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[P65VIEW:[0-9]+]] = cir.cast bitcast %[[P65SLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %{{[0-9]+}} = cir.load %[[P65VIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 65, bitint>>, !cir.int<s, 65, bitint>
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @take_widths(
+  // LLVM-SAME:    i17 signext %{{[0-9]+}}, i64 %[[A1:[0-9]+]]
+  // LLVM-SAME:    , i64 %[[A2:[0-9]+]], i64 %[[A3:[0-9]+]])
+  // The fourth register holds the 33-bit value, materialized first.
+  // LLVM-NEXT:    %[[W33SLOT:[0-9]+]] = alloca i64, i64 1, align 8
+  // LLVM-NEXT:    store i64 %[[A3]], ptr %[[W33SLOT]], align 8
+  // LLVM-NEXT:    %[[W33:[0-9]+]] = load i64, ptr %[[W33SLOT]], align 8
+  // LLVM-NEXT:    %{{[0-9]+}} = trunc i64 %[[W33]] to i33
+  // The second and third rebuild the 65-bit pair in order.
+  // LLVM-NEXT:    %[[P65SLOT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[FLAT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[E0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 0
+  // LLVM-NEXT:    store i64 %[[A1]], ptr %[[E0]], align 8
+  // LLVM-NEXT:    %[[E1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 1
+  // LLVM-NEXT:    store i64 %[[A2]], ptr %[[E1]], align 8
+  // LLVM-NEXT:    %[[PAIR:[0-9]+]] = load { i64, i64 }, ptr %[[FLAT]], align 8
+  // LLVM-NEXT:    store { i64, i64 } %[[PAIR]], ptr %[[P65SLOT]], align 8
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i128, ptr %[[P65SLOT]], align 8
+  // LLVM-NEXT:    %{{[0-9]+}} = trunc i128 %[[WIDE]] to i65
+  // LLVM-NEXT:    ret void
+
+  // Calling it maps the caller's four registers back onto the callee's four:
+  // the extended argument passes straight through, the pair is decomposed, and
+  // the 33-bit value is re-widened.
+  cir.func @call_widths(%arg0: !b17i, %arg1: !b65i, %arg2: !b33i) {
+    cir.call @take_widths(%arg0, %arg1, %arg2) : (!b17i, !b65i, !b33i) -> ()
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @call_widths(
+  // CHECK-SAME:    %arg0: !cir.int<s, 17, bitint> {llvm.signext}
+  // CHECK-SAME:    , %arg1: !u64i, %arg2: !u64i, %arg3: !u64i)
+  // CHECK-NEXT:    %[[V33SLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg3, %[[V33SLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[V33VIEW:[0-9]+]] = cir.cast bitcast %[[V33SLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[V33:[0-9]+]] = cir.load %[[V33VIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 33, bitint>>, !cir.int<s, 33, bitint>
+  // CHECK-NEXT:    %[[V65SLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[E0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[E0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[E1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg2, %[[E1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[PAIR]], %[[V65SLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[V65VIEW:[0-9]+]] = cir.cast bitcast %[[V65SLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[V65:[0-9]+]] = cir.load %[[V65VIEW]]
+  // CHECK-SAME:      : !cir.ptr<!cir.int<s, 65, bitint>>, !cir.int<s, 65, bitint>
+  // CHECK-NEXT:    %[[W33SLOT:[0-9]+]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIRSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[PAIRVIEW:[0-9]+]] = cir.cast bitcast %[[PAIRSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    cir.store %[[V65]], %[[PAIRVIEW]]
+  // CHECK-SAME:      : !cir.int<s, 65, bitint>, !cir.ptr<!cir.int<s, 65, bitint>>
+  // CHECK-NEXT:    %[[A1P:[0-9]+]] = cir.get_member %[[PAIRSLOT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[A1:[0-9]+]] = cir.load %[[A1P]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    %[[A2P:[0-9]+]] = cir.get_member %[[PAIRSLOT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[A2:[0-9]+]] = cir.load %[[A2P]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    %[[W33VIEW:[0-9]+]] = cir.cast bitcast %[[W33SLOT]]
+  // CHECK-SAME:      : !cir.ptr<!u64i> -> !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    cir.store %[[V33]], %[[W33VIEW]]
+  // CHECK-SAME:      : !cir.int<s, 33, bitint>, !cir.ptr<!cir.int<s, 33, bitint>>
+  // CHECK-NEXT:    %[[A3:[0-9]+]] = cir.load %[[W33SLOT]] : !cir.ptr<!u64i>, !u64i
+  // CHECK-NEXT:    cir.call @take_widths(%arg0, %[[A1]], %[[A2]], %[[A3]])
+  // CHECK-SAME:      : (!cir.int<s, 17, bitint> {llvm.signext}
+  // CHECK-SAME:      , !u64i, !u64i, !u64i) -> ()
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @call_widths(
+  // LLVM-SAME:    i17 signext %[[A0:[0-9]+]], i64 %[[A1:[0-9]+]]
+  // LLVM-SAME:    , i64 %[[A2:[0-9]+]], i64 %[[A3:[0-9]+]])
+  // LLVM-NEXT:    %[[V33SLOT:[0-9]+]] = alloca i64, i64 1, align 8
+  // LLVM-NEXT:    store i64 %[[A3]], ptr %[[V33SLOT]], align 8
+  // LLVM-NEXT:    %[[W33:[0-9]+]] = load i64, ptr %[[V33SLOT]], align 8
+  // LLVM-NEXT:    %[[V33:[0-9]+]] = trunc i64 %[[W33]] to i33
+  // LLVM-NEXT:    %[[V65SLOT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[FLAT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[E0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 0
+  // LLVM-NEXT:    store i64 %[[A1]], ptr %[[E0]], align 8
+  // LLVM-NEXT:    %[[E1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 1
+  // LLVM-NEXT:    store i64 %[[A2]], ptr %[[E1]], align 8
+  // LLVM-NEXT:    %[[PAIR:[0-9]+]] = load { i64, i64 }, ptr %[[FLAT]], align 8
+  // LLVM-NEXT:    store { i64, i64 } %[[PAIR]], ptr %[[V65SLOT]], align 8
+  // LLVM-NEXT:    %[[WIDE:[0-9]+]] = load i128, ptr %[[V65SLOT]], align 8
+  // LLVM-NEXT:    %[[V65:[0-9]+]] = trunc i128 %[[WIDE]] to i65
+  // Each callee register traces back to the value it carries.
+  // LLVM-NEXT:    %[[C3SLOT:[0-9]+]] = alloca i64, i64 1, align 8
+  // LLVM-NEXT:    %[[PAIRSLOT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[EXT65:[0-9]+]] = sext i65 %[[V65]] to i128
+  // LLVM-NEXT:    store i128 %[[EXT65]], ptr %[[PAIRSLOT]], align 8
+  // LLVM-NEXT:    %[[P0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[PAIRSLOT]], i32 0, i32 0
+  // LLVM-NEXT:    %[[C1:[0-9]+]] = load i64, ptr %[[P0]], align 8
+  // LLVM-NEXT:    %[[P1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[PAIRSLOT]], i32 0, i32 1
+  // LLVM-NEXT:    %[[C2:[0-9]+]] = load i64, ptr %[[P1]], align 8
+  // LLVM-NEXT:    %[[EXT33:[0-9]+]] = sext i33 %[[V33]] to i64
+  // LLVM-NEXT:    store i64 %[[EXT33]], ptr %[[C3SLOT]], align 8
+  // LLVM-NEXT:    %[[C3:[0-9]+]] = load i64, ptr %[[C3SLOT]], align 8
+  // LLVM-NEXT:    call void @take_widths(i17 signext %[[A0]], i64 %[[C1]]
+  // LLVM-SAME:      , i64 %[[C2]], i64 %[[C3]])
+  // LLVM-NEXT:    ret void
+
+  // Only a bit-precise type rounds up to a storage width, so a plain i40
+  // coerces to its own width and the widen check leaves it alone.
+  cir.func @plain_i40(%arg0: !cir.int<s, 40>) -> !cir.int<s, 40> {
+    cir.return %arg0 : !cir.int<s, 40>
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @plain_i40(%arg0: !cir.int<s, 40>)
+  // CHECK-SAME:    -> !cir.int<s, 40>
+  // CHECK-NEXT:    cir.return %arg0 : !cir.int<s, 40>
+
+  // LLVM-LABEL: define i40 @plain_i40(
+  // LLVM-SAME:    i40 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    ret i40 %[[ARG]]
+
+  // A struct holding one sub-eightbyte _BitInt is a single INTEGER eightbyte:
+  // Direct, coerced to i32, and reconstituted before the body's own alloca.
+  cir.func @coerce_s(%arg0: !rec_S) -> !rec_S {
+    %0 = cir.alloca "a" align(4) : !cir.ptr<!rec_S>
+    cir.store %arg0, %0 : !rec_S, !cir.ptr<!rec_S>
+    %1 = cir.load %0 : !cir.ptr<!rec_S>, !rec_S
+    cir.return %1 : !rec_S
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @coerce_s(%arg0: !s32i) -> !s32i
+  // CHECK-NEXT:    %[[RETSLOT:[0-9]+]] = cir.alloca "coerce" align(4) : !cir.ptr<!rec_S>
+  // CHECK-NEXT:    %[[ARGSLOT:[0-9]+]] = cir.alloca "coerce" align(4) : !cir.ptr<!s32i>
+  // CHECK-NEXT:    cir.store %arg0, %[[ARGSLOT]] : !s32i, !cir.ptr<!s32i>
+  // CHECK-NEXT:    %[[ARGVIEW:[0-9]+]] = cir.cast bitcast %[[ARGSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!s32i> -> !cir.ptr<!rec_S>
+  // CHECK-NEXT:    %[[REC:[0-9]+]] = cir.load %[[ARGVIEW]] : !cir.ptr<!rec_S>, !rec_S
+  // CHECK-NEXT:    %[[LOCAL:[0-9]+]] = cir.alloca "a" align(4) : !cir.ptr<!rec_S>
+  // CHECK-NEXT:    cir.store %[[REC]], %[[LOCAL]] : !rec_S, !cir.ptr<!rec_S>
+  // CHECK-NEXT:    %[[VAL:[0-9]+]] = cir.load %[[LOCAL]] : !cir.ptr<!rec_S>, !rec_S
+  // CHECK-NEXT:    cir.store %[[VAL]], %[[RETSLOT]] : !rec_S, !cir.ptr<!rec_S>
+  // CHECK-NEXT:    %[[RETVIEW:[0-9]+]] = cir.cast bitcast %[[RETSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_S> -> !cir.ptr<!s32i>
+  // CHECK-NEXT:    %[[RES:[0-9]+]] = cir.load %[[RETVIEW]] : !cir.ptr<!s32i>, !s32i
+  // CHECK-NEXT:    cir.return %[[RES]] : !s32i
+
+  // LLVM-LABEL: define i32 @coerce_s(
+  // LLVM-SAME:    i32 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    %[[RETSLOT:[0-9]+]] = alloca %struct.S, i64 1, align 4
+  // LLVM-NEXT:    %[[ARGSLOT:[0-9]+]] = alloca i32, i64 1, align 4
+  // LLVM-NEXT:    store i32 %[[ARG]], ptr %[[ARGSLOT]], align 4
+  // LLVM-NEXT:    %[[REC:[0-9]+]] = load %struct.S, ptr %[[ARGSLOT]], align 4
+  // LLVM-NEXT:    %[[LOCAL:[0-9]+]] = alloca %struct.S, i64 1, align 4
+  // LLVM-NEXT:    store %struct.S %[[REC]], ptr %[[LOCAL]], align 4
+  // LLVM-NEXT:    %[[VAL:[0-9]+]] = load %struct.S, ptr %[[LOCAL]], align 4
+  // LLVM-NEXT:    store %struct.S %[[VAL]], ptr %[[RETSLOT]], align 4
+  // LLVM-NEXT:    %[[RES:[0-9]+]] = load i32, ptr %[[RETSLOT]], align 4
+  // LLVM-NEXT:    ret i32 %[[RES]]
+
+  // A struct holding a two-eightbyte _BitInt flattens into an i64 pair.
+  cir.func @take_p(%arg0: !rec_P) {
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @take_p(%arg0: !u64i, %arg1: !u64i)
+  // CHECK-NEXT:    %[[VALSLOT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[FLAT:[0-9]+]] = cir.alloca "coerce" align(8)
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[E0:[0-9]+]] = cir.get_member %[[FLAT]][0]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg0, %[[E0]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[E1:[0-9]+]] = cir.get_member %[[FLAT]][1]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i>
+  // CHECK-NEXT:    cir.store %arg1, %[[E1]] : !u64i, !cir.ptr<!u64i>
+  // CHECK-NEXT:    %[[PAIR:[0-9]+]] = cir.load %[[FLAT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct>, !rec_anon_struct
+  // CHECK-NEXT:    cir.store %[[PAIR]], %[[VALSLOT]]
+  // CHECK-SAME:      : !rec_anon_struct, !cir.ptr<!rec_anon_struct>
+  // CHECK-NEXT:    %[[VIEW:[0-9]+]] = cir.cast bitcast %[[VALSLOT]]
+  // CHECK-SAME:      : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!rec_P>
+  // CHECK-NEXT:    %{{[0-9]+}} = cir.load %[[VIEW]] : !cir.ptr<!rec_P>, !rec_P
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @take_p(
+  // LLVM-SAME:    i64 %[[A0:[0-9]+]], i64 %[[A1:[0-9]+]])
+  // LLVM-NEXT:    %[[VALSLOT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[FLAT:[0-9]+]] = alloca { i64, i64 }, i64 1, align 8
+  // LLVM-NEXT:    %[[E0:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 0
+  // LLVM-NEXT:    store i64 %[[A0]], ptr %[[E0]], align 8
+  // LLVM-NEXT:    %[[E1:[0-9]+]] = getelementptr inbounds nuw { i64, i64 }
+  // LLVM-SAME:      , ptr %[[FLAT]], i32 0, i32 1
+  // LLVM-NEXT:    store i64 %[[A1]], ptr %[[E1]], align 8
+  // LLVM-NEXT:    %[[PAIR:[0-9]+]] = load { i64, i64 }, ptr %[[FLAT]], align 8
+  // LLVM-NEXT:    store { i64, i64 } %[[PAIR]], ptr %[[VALSLOT]], align 8
+  // LLVM-NEXT:    %{{[0-9]+}} = load %struct.P, ptr %[[VALSLOT]], align 16
+  // LLVM-NEXT:    ret void
+
+  // A struct holding a _BitInt(128) coerces to a single i128.
+  cir.func @take_w(%arg0: !rec_W) {
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @take_w(%arg0: !s128i)
+  // CHECK-NEXT:    %[[SLOT:[0-9]+]] = cir.alloca "coerce" align(16) : !cir.ptr<!s128i>
+  // CHECK-NEXT:    cir.store %arg0, %[[SLOT]] : !s128i, !cir.ptr<!s128i>
+  // CHECK-NEXT:    %[[VIEW:[0-9]+]] = cir.cast bitcast %[[SLOT]]
+  // CHECK-SAME:      : !cir.ptr<!s128i> -> !cir.ptr<!rec_W>
+  // CHECK-NEXT:    %{{[0-9]+}} = cir.load %[[VIEW]] : !cir.ptr<!rec_W>, !rec_W
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @take_w(
+  // LLVM-SAME:    i128 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    %[[SLOT:[0-9]+]] = alloca i128, i64 1, align 16
+  // LLVM-NEXT:    store i128 %[[ARG]], ptr %[[SLOT]], align 16
+  // LLVM-NEXT:    %{{[0-9]+}} = load %struct.W, ptr %[[SLOT]]
+  // LLVM-NEXT:    ret void
+
+  // A trailing byte pushes the struct past two eightbytes, so it passes byval,
+  // aligned 8 as _BitInt(128) requires rather than __int128's 16.
+  cir.func @take_o(%arg0: !rec_O) {
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @take_o(
+  // CHECK-SAME:    %arg0: !cir.ptr<!rec_O> {llvm.align = 8 : i64
+  // CHECK-SAME:    , llvm.byval = !rec_O, llvm.noalias, llvm.noundef})
+  // CHECK-NEXT:    %{{[0-9]+}} = cir.load %arg0 : !cir.ptr<!rec_O>, !rec_O
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @take_o(
+  // LLVM-SAME:    ptr noalias noundef byval(%struct.O) align 8 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    %{{[0-9]+}} = load %struct.O, ptr %[[ARG]]
+  // LLVM-NEXT:    ret void
+
+  // Returning it takes the sret path, and the caller's buffer replaces the
+  // return slot the body allocated.
+  cir.func @ret_o() -> !rec_O {
+    %0 = cir.alloca "__retval" align(8) : !cir.ptr<!rec_O>
+    %1 = cir.const #cir.zero : !rec_O
+    cir.store %1, %0 : !rec_O, !cir.ptr<!rec_O>
+    %2 = cir.load %0 : !cir.ptr<!rec_O>, !rec_O
+    cir.return %2 : !rec_O
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @ret_o(
+  // CHECK-SAME:    %arg0: !cir.ptr<!rec_O> {llvm.align = 8 : i64
+  // CHECK-SAME:    , llvm.dead_on_unwind, llvm.noalias, llvm.sret = !rec_O
+  // CHECK-SAME:    , llvm.writable})
+  // CHECK-NEXT:    %[[Z:[0-9]+]] = cir.const #cir.zero : !rec_O
+  // CHECK-NEXT:    cir.store %[[Z]], %arg0 : !rec_O, !cir.ptr<!rec_O>
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @ret_o(
+  // LLVM-SAME:    ptr dead_on_unwind noalias writable sret(%struct.O) align 8
+  // LLVM-SAME:    %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    store %struct.O zeroinitializer, ptr %[[ARG]]
+  // LLVM-NEXT:    ret void
+
+  // Each _BitInt(65) element occupies two eightbytes, so an array of two is 32
+  // bytes and the wrapping struct passes indirectly.
+  cir.func @take_arr(%arg0: !rec_Arr) {
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func{{.*}} @take_arr(
+  // CHECK-SAME:    %arg0: !cir.ptr<!rec_Arr> {llvm.align = 8 : i64
+  // CHECK-SAME:    , llvm.byval = !rec_Arr, llvm.noalias, llvm.noundef})
+  // CHECK-NEXT:    %{{[0-9]+}} = cir.load %arg0 : !cir.ptr<!rec_Arr>, !rec_Arr
+  // CHECK-NEXT:    cir.return
+
+  // LLVM-LABEL: define void @take_arr(
+  // LLVM-SAME:    ptr noalias noundef byval(%struct.Arr) align 8 %[[ARG:[0-9]+]])
+  // LLVM-NEXT:    %{{[0-9]+}} = load %struct.Arr, ptr %[[ARG]]
+  // LLVM-NEXT:    ret void
+}

diff  --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-int-nyi.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-int-nyi.cir
index eb023d8f8c586..ebda399c0d081 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-int-nyi.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-int-nyi.cir
@@ -1,8 +1,9 @@
 // RUN: not cir-opt %s -cir-call-conv-lowering=target=x86_64 2>&1 | FileCheck %s
 
-!b17i = !cir.int<s, 17, bitint>
-!b128i = !cir.int<s, 128, bitint>
+!b129i = !cir.int<s, 129, bitint>
+!b200i = !cir.int<s, 200, bitint>
 !i96 = !cir.int<s, 96>
+!rec_D = !cir.struct<"D" {!b200i}>
 
 module attributes {
   dlti.dl_spec = #dlti.dl_spec<
@@ -11,26 +12,29 @@ module attributes {
     #dlti.dl_entry<i128, dense<128>: vector<2xi64>>>
 } {
 
-  // A _BitInt is rejected pending its register-pair coercion handling, even at
-  // a sub-register width.
-  cir.func @take_b17(%arg0: !b17i) {
+  // 129 bits is the first rejected width.
+  cir.func @take_b129(%arg0: !b129i) {
     cir.return
   }
 
-  // CHECK: not yet implemented for type '!cir.int<s, 17, bitint>
+  // CHECK: not yet implemented for type '!cir.int<s, 129, bitint>
 
-  // A _BitInt(128) shares __int128's width but stays rejected: the accept check
-  // excludes bitint widths regardless of size.
-  cir.func @take_b128(%arg0: !b128i) {
+  cir.func @take_b200(%arg0: !b200i) {
     cir.return
   }
 
-  // CHECK: not yet implemented for type '!cir.int<s, 128, bitint>
+  // CHECK: not yet implemented for type '!cir.int<s, 200, bitint>
 
-  // A non-bitint integer with an intermediate width (65..127) is also rejected:
-  // it does not arise from C and would need a register-pair coercion the scalar
-  // Direct branch does not apply, so only widths up to 64 and exactly 128 are
-  // accepted.
+  // A struct carrying an over-wide _BitInt is rejected through the same
+  // recursive field check.
+  cir.func @take_d(%arg0: !rec_D) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.struct<"D"
+
+  // A non-bitint integer of intermediate width (65..127) is rejected: only
+  // widths up to 64 and exactly 128 are accepted.
   cir.func @take_i96(%arg0: !i96) {
     cir.return
   }


        


More information about the cfe-commits mailing list