[clang] [CIR] Emit classic's attributes for a non-byval indirect argument (PR #222445)

Adam Smith via cfe-commits cfe-commits at lists.llvm.org
Thu Sep 10 13:02:56 PDT 2026


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

>From 2c5657815a9cddd64eb9d409d41218f12c55f320 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 9 Sep 2026 13:47:58 -0700
Subject: [PATCH 1/3] [CIR] Emit classic's attributes for a non-byval indirect
 argument

CallConvLowering marked an indirect argument that is not byval with
llvm.byref.  Classic gives this argument llvm.nofreeobj, llvm.noundef,
llvm.align, and llvm.dereferenceable instead.

A call site forwarding such a parameter used to identify it by reading
llvm.byref back, so the pass now records the parameter where the
signature rewrite creates it.  The "byref" vocabulary is removed, since
it no longer names anything CIR emits.

Assisted-by: Cursor / claude-opus-5
---
 .../TargetLowering/CIRABIRewriteContext.cpp   | 213 ++++++++--------
 .../TargetLowering/CIRABIRewriteContext.h     |  16 +-
 clang/test/CIR/CodeGen/array-ctor.cpp         |  12 +-
 ...> call-conv-lowering-x86_64-non-byval.cpp} |  82 +++---
 .../call-conv-lowering-x86_64-vptr.cpp        |  32 +--
 .../cleanup-conditional-with-wrapper-eh.cpp   |  12 +-
 .../cleanup-conditional-with-wrapper.cpp      |  12 +-
 clang/test/CIR/CodeGen/lambda-dtor-field.cpp  |  10 +-
 .../CIR/CodeGen/partial-array-cleanup.cpp     |   6 +-
 .../test/CIR/CodeGenOpenACC/declare-copy.cpp  |   6 +-
 .../CIR/CodeGenOpenACC/declare-copyin.cpp     |   6 +-
 .../CIR/CodeGenOpenACC/declare-copyout.cpp    |   6 +-
 .../CIR/CodeGenOpenACC/declare-create.cpp     |   6 +-
 .../CodeGenOpenACC/declare-deviceresident.cpp |   6 +-
 .../CIR/CodeGenOpenACC/declare-present.cpp    |   6 +-
 .../byval-sret-arg-attr-lowering.cir          |  10 +-
 .../abi-lowering/indirect-byval.cir           | 186 +++++++-------
 ...r => indirect-non-byval-forward-param.cir} | 144 +++++++----
 ...ref-nyi.cir => indirect-non-byval-nyi.cir} | 240 +++++++++++-------
 .../abi-lowering/x86_64-empty-class.cir       |   4 +-
 .../abi-lowering/x86_64-record-packed.cir     |   8 +-
 .../abi-lowering/x86_64-struct-indirect.cir   |   2 +-
 .../abi-lowering/x86_64-struct-padded.cir     |   5 +-
 .../Transforms/abi-lowering/x86_64-union.cir  |   4 +-
 .../Transforms/abi-lowering/x86_64-vptr.cir   |   8 +-
 25 files changed, 566 insertions(+), 476 deletions(-)
 rename clang/test/CIR/CodeGen/{call-conv-lowering-x86_64-byref.cpp => call-conv-lowering-x86_64-non-byval.cpp} (65%)
 rename clang/test/CIR/Transforms/abi-lowering/{indirect-byref-forward-param.cir => indirect-non-byval-forward-param.cir} (51%)
 rename clang/test/CIR/Transforms/abi-lowering/{indirect-byref-nyi.cir => indirect-non-byval-nyi.cir} (56%)

diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index e2f24e9c4467e..a8070decedcf8 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -21,19 +21,20 @@ using namespace mlir::abi;
 
 // This rewrite context supports the Direct (with or without coercion),
 // Extend, Ignore, Indirect-return (sret), Indirect-argument (byval and
-// byref), and Expand (struct flattening) classifications.
+// non-byval), and Expand (struct flattening) classifications.
 //
-// "byref" here is the llvm.byref case of an Indirect argument, not C++
-// pass-by-reference.  A C++ reference parameter is already a pointer by the
-// time this classifier runs, so it classifies Direct.  byref instead means
-// a by-value parameter whose type cannot be copied freely, because it has a
-// non-trivial copy constructor, move constructor, or destructor, so the ABI
-// passes it through a pointer instead of in registers.
+// An Indirect argument is byval or not, following its classification's
+// byVal flag.  byval is a by-value parameter the ABI passes in memory rather
+// than registers, usually for its size.  Non-byval is a by-value parameter
+// whose type cannot be copied freely, because it has a non-trivial copy
+// constructor, move constructor, or destructor, so the callee works on the
+// caller's own object rather than a copy.
 //
-// At the call site byval copies into a fresh alloca while byref forwards
-// the caller's storage.  At the callee, byval loads the incoming pointer
-// (a local copy), while byref rewires the CIRGen param-slot alloca to the
-// incoming pointer so the body mutates the caller's storage in place.
+// At the call site byval copies into a fresh alloca while a non-byval
+// argument forwards the caller's storage.  At the callee, byval loads the
+// incoming pointer (a local copy), while non-byval rewires the CIRGen
+// param-slot alloca to the incoming pointer so the body mutates the caller's
+// storage in place.
 //
 // For Expand, the single struct argument is replaced by N scalar arguments
 // (one per field).  At the callee, the N field block arguments are stored
@@ -73,7 +74,7 @@ cir::RecordType getFlattenedCoercedType(const ArgClassification &ac) {
 
 /// Build the new argument-type list for a function whose ABI classification
 /// is \p fc.  Handles Direct (with or without coercion), Extend, Ignore,
-/// Indirect (byval and byref), and Expand (struct flattening) arguments.
+/// Indirect (byval and non-byval), and Expand (struct flattening) arguments.
 /// The sret return pointer, when present, is prepended by
 /// rewriteFunctionDefinition rather than here.
 mlir::LogicalResult
@@ -125,8 +126,6 @@ buildNewArgTypes(ArrayRef<mlir::Type> oldArgTypes,
       newArgTypes.push_back(origTy);
       break;
     case ArgKind::Indirect:
-      // byval and byref both pass a pointer.  Which of the two it is shows up
-      // in the attributes updateArgAttrs applies, not in the type.
       newArgTypes.push_back(cir::PointerType::get(origTy));
       break;
     }
@@ -179,14 +178,15 @@ mlir::Value createIgnoredValue(mlir::OpBuilder &builder, mlir::Location loc,
 }
 
 /// Build an updated arg_attrs ArrayAttr that drops Ignore'd args, adds
-/// llvm.signext / llvm.zeroext on Extend args, and adds llvm.byval /
-/// llvm.align on Indirect args.  Preserves any existing arg attributes on
+/// llvm.signext / llvm.zeroext on Extend args, and adds the pointer
+/// attributes for Indirect args.  Preserves any existing arg attributes on
 /// retained arg slots.  \p origArgTypes provides the pre-rewrite type for
-/// each arg slot (needed to compute the llvm.byval pointee type).
+/// each arg slot.
 mlir::ArrayAttr updateArgAttrs(mlir::MLIRContext *ctx,
                                ArrayRef<mlir::Type> origArgTypes,
                                mlir::ArrayAttr existingArgAttrs,
-                               const FunctionClassification &fc) {
+                               const FunctionClassification &fc,
+                               const mlir::DataLayout &dl) {
   mlir::Builder builder(ctx);
   SmallVector<mlir::Attribute> newArgAttrs;
   newArgAttrs.reserve(fc.argInfos.size());
@@ -214,30 +214,35 @@ mlir::ArrayAttr updateArgAttrs(mlir::MLIRContext *ctx,
       attrs.set(attrName, builder.getUnitAttr());
       newArgAttrs.push_back(attrs.getDictionary(ctx));
     } else if (ac.kind == ArgKind::Indirect) {
-      // byval: caller-allocated copy; callee receives pointer to copy.
-      // byref: callee receives pointer to the caller's original storage.
-      // Both use llvm.align(A).  The ownership flag differs: llvm.byval(T)
-      // vs llvm.byref(T).  Both are typed attributes carrying the pointee
-      // type T (the pre-rewrite arg type); T is recorded explicitly because
-      // it cannot be recovered from the opaque LLVM pointer after lowering.
+      // byval hands the callee its own copy.  Without byval it gets a pointer
+      // to the caller's own object.  Both state llvm.align and llvm.noundef,
+      // which constrains the pointer operand, not the pointee's contents.
       //
-      // byval also gets llvm.noundef: the caller's original must be defined
-      // or UB has already occurred, and the copy inherits that.
+      // llvm.byval(T) records the pre-rewrite arg type because the opaque
+      // LLVM pointer cannot carry it.  llvm.nofreeobj holds because a
+      // parameter has automatic storage duration.
       //
-      // byval does not get llvm.noalias.  Classic adds it only under
-      // -fpass-by-value-is-noalias for a record that can pass in registers,
-      // and that option is not plumbed into CIR.
+      // Two of classic's attributes are missing.  llvm.noalias needs
+      // -fpass-by-value-is-noalias, which CIR does not plumb through.
+      // llvm.dead_on_return needs the destructor's triviality, which
+      // cir.record_layout carries as has_trivial_dtor and nothing here reads
+      // yet.
       mlir::Type pointeeTy = origArgTypes[oldIdx];
-      StringRef ownershipAttr =
-          ac.byVal ? mlir::LLVM::LLVMDialect::getByValAttrName()
-                   : mlir::LLVM::LLVMDialect::getByRefAttrName();
       mlir::NamedAttrList attrs(existing);
       attrs.set(mlir::LLVM::LLVMDialect::getAlignAttrName(),
                 builder.getI64IntegerAttr(ac.indirectAlign.value()));
-      attrs.set(ownershipAttr, mlir::TypeAttr::get(pointeeTy));
-      if (ac.byVal)
-        attrs.set(mlir::LLVM::LLVMDialect::getNoUndefAttrName(),
+      attrs.set(mlir::LLVM::LLVMDialect::getNoUndefAttrName(),
+                builder.getUnitAttr());
+      if (ac.byVal) {
+        attrs.set(mlir::LLVM::LLVMDialect::getByValAttrName(),
+                  mlir::TypeAttr::get(pointeeTy));
+      } else {
+        attrs.set(mlir::LLVM::LLVMDialect::getNoFreeObjAttrName(),
                   builder.getUnitAttr());
+        attrs.set(mlir::LLVM::LLVMDialect::getDereferenceableAttrName(),
+                  builder.getI64IntegerAttr(
+                      dl.getTypeSize(pointeeTy).getFixedValue()));
+      }
       newArgAttrs.push_back(attrs.getDictionary(ctx));
     } else {
       newArgAttrs.push_back(existing);
@@ -444,37 +449,28 @@ static cir::LoadOp getWholeRecordLoad(mlir::Value recordVal) {
   return load;
 }
 
-/// Whether \p addr is the enclosing function's own byref parameter, stated to
-/// be at least \p minAlign aligned.  Passing that pointer on as byref tells
-/// the callee nothing the incoming byref did not already state, so it can be
-/// handed on unchanged.  The other pointer parameters also state an
-/// alignment, so it is llvm.byref that identifies this one.
-static bool isByrefParameter(mlir::Value addr, uint64_t minAlign) {
+/// Whether a non-byval indirect argument may name \p addr, given the callee is
+/// told the argument is \p minAlign aligned.  A slot allocated here qualifies,
+/// reached through storage-preserving casts, and so does a non-byval indirect
+/// parameter of the enclosing function, matched by exact value.  Which arm
+/// answers depends on whether the driver reached this call before the
+/// definition enclosing it.  For CIRGen output they agree, since the spill
+/// slot states the alignment the classification does.
+///
+/// Both must already state that alignment: a slot's own alignment can be
+/// raised in principle, but one standing in for a parameter is replaced by the
+/// incoming pointer later, which would discard the raise and leave the callee
+/// over-promised.
+static bool forwardableNonByvalStorage(
+    mlir::Value addr, uint64_t minAlign,
+    const llvm::DenseMap<mlir::BlockArgument, uint64_t> &nonByvalParams) {
+  if (cir::AllocaOp slot = cir::getUnderlyingAlloca(addr))
+    return slot.getAlignment() >= minAlign;
   auto blockArg = mlir::dyn_cast<mlir::BlockArgument>(addr);
   if (!blockArg || !blockArg.getOwner()->isEntryBlock())
     return false;
-  auto funcOp = mlir::dyn_cast<cir::FuncOp>(blockArg.getOwner()->getParentOp());
-  if (!funcOp)
-    return false;
-  unsigned argNo = blockArg.getArgNumber();
-  if (!funcOp.getArgAttr(argNo, mlir::LLVM::LLVMDialect::getByRefAttrName()))
-    return false;
-  auto align = funcOp.getArgAttrOfType<mlir::IntegerAttr>(
-      argNo, mlir::LLVM::LLVMDialect::getAlignAttrName());
-  return align && align.getValue().getZExtValue() >= minAlign;
-}
-
-/// Whether a byref argument may name \p addr, given the callee is told the
-/// argument is \p minAlign aligned.  A slot allocated here qualifies,
-/// including one reached through storage-preserving casts, and so does the
-/// enclosing function's own byref parameter.  Both must already state
-/// that alignment: a slot's own alignment can be raised in principle, but one
-/// standing in for a parameter is replaced by the incoming pointer later, which
-/// would discard the raise and leave the callee over-promised.
-static bool forwardableByrefStorage(mlir::Value addr, uint64_t minAlign) {
-  if (cir::AllocaOp slot = cir::getUnderlyingAlloca(addr))
-    return slot.getAlignment() >= minAlign;
-  return isByrefParameter(addr, minAlign);
+  auto param = nonByvalParams.find(blockArg);
+  return param != nonByvalParams.end() && param->second >= minAlign;
 }
 
 /// Decompose a struct value into one scalar call argument per field of \p
@@ -531,22 +527,22 @@ static void eraseDeadRecordLoads(ArrayRef<cir::LoadOp> loads) {
 /// back to the original type for body uses.  For each Indirect byval arg,
 /// change the block argument's type to a pointer and insert a load at entry
 /// so the body sees a local copy of the original value type.  For each
-/// Indirect byref arg, change the block argument to a pointer and rewire the
-/// CIRGen param-slot alloca to that pointer (no entry load / byte-copy) so
-/// the body operates on the caller's storage in place.  For each Expand arg,
-/// replace the single struct block argument with N scalar block arguments (one
-/// per field) and store each field directly into the parameter's own alloca
-/// (the CIRGen spill slot), erasing the original whole-struct store.
+/// Indirect non-byval arg, change the block argument to a pointer and
+/// rewire the CIRGen param-slot alloca to that pointer (no entry load /
+/// byte-copy) so the body operates on the caller's storage in place.  For each
+/// Expand arg, replace the single struct block argument with N scalar block
+/// arguments (one per field) and store each field directly into the parameter's
+/// own alloca (the CIRGen spill slot), erasing the original whole-struct store.
 ///
 /// \p hasSRetArg is true when the function has an sret return (a hidden return
 /// pointer is prepended as block argument 0).  Expand arguments expand the
 /// block argument count, so a running index tracks the current block argument
 /// position rather than computing the classification index + \p hasSRetArg
 /// directly.
-void insertArgCoercion(mlir::FunctionOpInterface funcOp,
-                       const FunctionClassification &fc,
-                       mlir::OpBuilder &builder, const mlir::DataLayout &dl,
-                       bool hasSRetArg) {
+void insertArgCoercion(
+    mlir::FunctionOpInterface funcOp, const FunctionClassification &fc,
+    mlir::OpBuilder &builder, const mlir::DataLayout &dl, bool hasSRetArg,
+    llvm::DenseMap<mlir::BlockArgument, uint64_t> &nonByvalParams) {
   mlir::Region &body = funcOp->getRegion(0);
   if (body.empty())
     return;
@@ -713,19 +709,19 @@ void insertArgCoercion(mlir::FunctionOpInterface funcOp,
       // to adapted (now of the original type != the alloca's pointee type).
       blockArg.replaceAllUsesExcept(adapted, coercionOps);
     } else if (ac.kind == ArgKind::Indirect) {
-      // byval and byref share a !cir.ptr<T> wire type; the llvm.byval vs
-      // llvm.byref distinction is in the attrs applied by updateArgAttrs.
-      // Body lowering differs: byval copies into the callee (load at entry),
-      // while byref must operate on the caller's storage in place.
+      // byval and non-byval both lower to !cir.ptr<T>, and which it is shows
+      // up only in the attrs updateArgAttrs applies.  Body lowering differs:
+      // byval copies into the callee (load at entry), while non-byval must
+      // operate on the caller's storage in place.
       auto ptrTy = cir::PointerType::get(blockArg.getType());
 
       if (!ac.byVal) {
-        // byref: CIRGen spills every by-value parameter into a local alloca
-        // with a single store before any other use, and CallConvLowering runs
-        // on that CIRGen output before any alloca-promoting/splitting pass, so
-        // the block argument still has exactly that one use here.  Rewire the
-        // alloca to the incoming pointer and drop the store so the body
-        // operates on the caller's storage in place.  A byte-copy would be
+        // Without byval: CIRGen spills every by-value parameter into a local
+        // alloca with a single store before any other use, and this pass runs
+        // on that CIRGen output before any alloca-promoting or splitting pass,
+        // so the block argument still has exactly that one use here.
+        // Rewire the alloca to the incoming pointer and drop the store so the
+        // body operates on the caller's storage in place.  A byte-copy would be
         // wrong for non-trivially-copyable aggregates (e.g. libstdc++ SSO
         // std::string, where it would leave `_M_p` aliasing the source's
         // `_M_local_buf`).  DCE may have removed a dead spill; tolerate that by
@@ -734,11 +730,11 @@ void insertArgCoercion(mlir::FunctionOpInterface funcOp,
         cir::AllocaOp destAlloca;
         if (!blockArg.use_empty()) {
           assert(blockArg.hasOneUse() &&
-                 "byref arg must have exactly one use (the CIRGen param "
+                 "non-byval arg must have exactly one use (the CIRGen param "
                  "spill)");
           paramStore = cast<cir::StoreOp>(*blockArg.user_begin());
           assert(paramStore.getValue() == blockArg &&
-                 "byref arg's use must be the value operand of its store");
+                 "non-byval arg's use must be the value operand of its store");
           destAlloca =
               cast<cir::AllocaOp>(paramStore.getAddr().getDefiningOp());
         }
@@ -749,6 +745,8 @@ void insertArgCoercion(mlir::FunctionOpInterface funcOp,
         // Update the block argument to point to its original type.
         blockArg.setType(ptrTy);
 
+        nonByvalParams[blockArg] = ac.indirectAlign.value();
+
         if (destAlloca) {
           destAlloca.getResult().replaceAllUsesWith(blockArg);
           destAlloca->erase();
@@ -931,7 +929,8 @@ void rewriteIndirectReturnCall(cir::CallOp call,
                                ArrayRef<mlir::Value> newArgs,
                                mlir::Type origRetTy,
                                ArrayRef<mlir::Type> origCallArgTypes,
-                               mlir::OpBuilder &builder) {
+                               mlir::OpBuilder &builder,
+                               const mlir::DataLayout &dl) {
   mlir::MLIRContext *ctx = call->getContext();
   auto ptrTy = cir::PointerType::get(origRetTy);
   builder.setInsertionPoint(call);
@@ -993,7 +992,7 @@ void rewriteIndirectReturnCall(cir::CallOp call,
                getFlattenedCoercedType(ac);
       });
   if (needsArgAttrUpdate)
-    argAttrs = updateArgAttrs(ctx, origCallArgTypes, argAttrs, fc);
+    argAttrs = updateArgAttrs(ctx, origCallArgTypes, argAttrs, fc, dl);
   applySretSlotAttrs(newCall, argAttrs, origRetTy, sretAlign, builder);
 
   if (reuseStore) {
@@ -1084,7 +1083,7 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
       // in-body cir.call operands) through the recovered value.  Done before
       // the Ignore-drop below so the entry block argument indices used here
       // still refer to the original positions.
-      insertArgCoercion(funcOp, fc, builder, dl, hasSRet);
+      insertArgCoercion(funcOp, fc, builder, dl, hasSRet, nonByvalParams);
 
       // Direct return with coerced type: insert a coercion at every
       // cir.return so the returned value matches the (coerced) return
@@ -1153,9 +1152,9 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
 
   // Rebuild arg_attrs when the function has an sret slot (slot 0 needs the
   // sret attribute set) or any arg is Ignore (dropped from the output array),
-  // Extend (needs llvm.signext / llvm.zeroext), Indirect (needs
-  // llvm.byval / llvm.align), Expand or Direct+canFlatten (both change the
-  // argument count).
+  // Extend (needs llvm.signext / llvm.zeroext), Indirect (gains the pointer
+  // attributes updateArgAttrs applies), Expand or Direct+canFlatten (both
+  // change the argument count).
   bool needsArgAttrUpdate =
       hasSRet || llvm::any_of(fc.argInfos, [](const ArgClassification &ac) {
         return ac.kind == ArgKind::Ignore || ac.kind == ArgKind::Extend ||
@@ -1164,7 +1163,8 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
       });
   if (needsArgAttrUpdate) {
     auto existing = funcOp->getAttrOfType<mlir::ArrayAttr>("arg_attrs");
-    mlir::ArrayAttr updated = updateArgAttrs(ctx, oldArgTypes, existing, fc);
+    mlir::ArrayAttr updated =
+        updateArgAttrs(ctx, oldArgTypes, existing, fc, dl);
     if (hasSRet) {
       // Prepend the sret slot's attribute dict (slot 0); the per-argument
       // dicts shift to slots 1..N.  noalias is valid only on the callee's
@@ -1232,9 +1232,9 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
   newArgs.reserve(argOperands.size());
 
   // Loads that the new call leaves unused: Expand and Direct+canFlatten read
-  // the fields out of the source alloca, and byref passes the address the
-  // load read from.
-  // The old call still uses them, so erase them only after it is gone.
+  // the fields out of the source alloca, and a non-byval argument passes the
+  // address the load read from.  The old call still uses them, so erase them
+  // only after it is gone.
   SmallVector<cir::LoadOp> deadRecordLoads;
 
   // Capture original arg types before building newArgs (byval slots change
@@ -1283,11 +1283,11 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
                          dl);
       newArgs.push_back(arg);
     } else if (ac.kind == ArgKind::Indirect) {
-      // byval hands the callee its own copy.  byref must name the caller's
-      // storage instead, so that the object the callee operates on is the one
-      // the caller destroys.  That means forwarding the address the operand
-      // was loaded from rather than the loaded value, so a store to that
-      // storage after the load is visible to the callee.
+      // byval hands the callee its own copy.  Without byval the argument must
+      // name the caller's storage instead, so that the object the callee
+      // operates on is the one the caller destroys.  That means forwarding
+      // the address the operand was loaded from rather than the loaded value,
+      // so a store to that storage after the load is visible to the callee.
       if (!ac.byVal) {
         // The rewritten parameter is a pointer to the argument type in the
         // default address space, so an operand read through an address-space
@@ -1297,11 +1297,12 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
         if (!srcLoad ||
             srcLoad.getAddr().getType() !=
                 cir::PointerType::get(arg.getType()) ||
-            !forwardableByrefStorage(srcLoad.getAddr(),
-                                     ac.indirectAlign.value()))
+            !forwardableNonByvalStorage(
+                srcLoad.getAddr(), ac.indirectAlign.value(), nonByvalParams))
           return call->emitOpError()
-                 << "byref argument that does not name the caller's storage "
-                    "is not yet implemented in CallConvLowering";
+                 << "non-byval indirect argument that does not name the "
+                    "caller's storage is not yet implemented in "
+                    "CallConvLowering";
         newArgs.push_back(srcLoad.getAddr());
         deadRecordLoads.push_back(srcLoad);
         continue;
@@ -1327,7 +1328,7 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
   // dedicated helper for it; everything below handles the by-value returns.
   if (fc.returnInfo.kind == ArgKind::Indirect && hasResult) {
     rewriteIndirectReturnCall(call, fc, newArgs, origRetTy, origCallArgTypes,
-                              builder);
+                              builder, dl);
     eraseDeadRecordLoads(deadRecordLoads);
     return mlir::success();
   }
@@ -1371,7 +1372,7 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
   if (needsArgAttrUpdate) {
     auto existing = call->getAttrOfType<mlir::ArrayAttr>("arg_attrs");
     newCall->setAttr("arg_attrs",
-                     updateArgAttrs(ctx, origCallArgTypes, existing, fc));
+                     updateArgAttrs(ctx, origCallArgTypes, existing, fc, dl));
   }
   if (fc.returnInfo.kind == ArgKind::Extend) {
     auto existing = call->getAttrOfType<mlir::ArrayAttr>("res_attrs");
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h
index 705036fc2f620..7d977cf026c5b 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h
@@ -12,7 +12,7 @@
 // the ABI-lowered shape.
 //
 // This file handles Direct (pass-through and coerce-in-registers), Extend,
-// Ignore, Indirect (sret return, byval and byref arguments), and Expand
+// Ignore, Indirect (sret return, byval and non-byval arguments), and Expand
 // (struct flattening into scalar fields).
 //
 //===----------------------------------------------------------------------===//
@@ -24,6 +24,7 @@
 #include "mlir/IR/BuiltinOps.h"
 #include "mlir/Interfaces/DataLayoutInterfaces.h"
 #include "clang/CIR/Dialect/IR/CIRDialect.h"
+#include "llvm/ADT/DenseMap.h"
 
 namespace cir {
 
@@ -63,6 +64,19 @@ class CIRABIRewriteContext : public mlir::abi::ABIRewriteContext {
 private:
   mlir::ModuleOp module;
   const mlir::DataLayout &dl;
+
+  /// Each block argument rewriteFunctionDefinition has rewritten into a
+  /// non-byval indirect parameter, mapped to the alignment its classification
+  /// states, so rewriteCallSite can forward such a parameter rather than copy
+  /// it.  Recorded where the classification says so rather than read back
+  /// from an emitted attribute, which would tie the pass to whichever
+  /// attribute is unique to this case today.
+  ///
+  /// Sound for one run over one module only.  A recorded argument is retyped
+  /// but never erased, so the keys stay valid, but a value freed with one
+  /// module can be recycled by the next, and a stale hit would forward the
+  /// caller's object where a copy is required.  Do not promote to pass state.
+  llvm::DenseMap<mlir::BlockArgument, uint64_t> nonByvalParams;
 };
 
 } // namespace cir
diff --git a/clang/test/CIR/CodeGen/array-ctor.cpp b/clang/test/CIR/CodeGen/array-ctor.cpp
index 07deb299542aa..16eb8a57ee6d8 100644
--- a/clang/test/CIR/CodeGen/array-ctor.cpp
+++ b/clang/test/CIR/CodeGen/array-ctor.cpp
@@ -206,7 +206,7 @@ void TempInArray() {
 // CIR:        cir.do {
 // CIR-NEXT:     %[[CURRENT:.*]] = cir.load %[[ITER:.*]] : !cir.ptr<!cir.ptr<!rec_CausesTemp>>, !cir.ptr<!rec_CausesTemp>
 // CIR-NEXT:     cir.cleanup.scope {
-// CIR-NEXT:       cir.call @_ZN10CausesTempC1E4Temp(%[[CURRENT]], %[[TMP]]) : ({{.*}}, !cir.ptr<!rec_Temp> {llvm.align = 1 : i64, llvm.byref = !rec_Temp}) -> ()
+// CIR-NEXT:       cir.call @_ZN10CausesTempC1E4Temp(%[[CURRENT]], %[[TMP]]) : ({{.*}}, !cir.ptr<!rec_Temp> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR-NEXT:       cir.yield
 // CIR-NEXT:     } cleanup normal {
 // CIR-NEXT:       cir.call @_ZN4TempD1Ev(%[[TMP]]) nothrow
@@ -227,7 +227,7 @@ void TempInArray() {
 // LLVM:       %[[CURRENT:.*]] = load ptr, ptr %[[ITER]]
 // LLVM:       br label %[[CONSTRUCT_BR:.*]]
 // LLVM:       [[CONSTRUCT_BR]]:
-// LLVM:       call void @_ZN10CausesTempC1E4Temp(ptr {{.*}}%[[CURRENT]], ptr byref(%struct.Temp) align 1 %[[TMP]])
+// LLVM:       call void @_ZN10CausesTempC1E4Temp(ptr {{.*}}%[[CURRENT]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[TMP]])
 // LLVM:       br label %[[CLEANUP_BR:.*]]
 // LLVM:       [[CLEANUP_BR]]:
 // LLVM:       call void @_ZN4TempD1Ev({{.*}}[[TMP]])
@@ -237,7 +237,7 @@ void TempInArray() {
 // OGCG:       br label %[[LOOP:.*]]
 // OGCG:       [[LOOP]]:
 // OGCG:       %[[CURRENT:.*]] = phi ptr
-// OGCG:       call void @_ZN10CausesTempC1E4Temp(ptr {{.*}}%[[CURRENT]], ptr{{.*}}[[TMP]])
+// OGCG:       call void @_ZN10CausesTempC1E4Temp(ptr {{.*}}%[[CURRENT]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[TMP]])
 // OGCG:       call void @_ZN4TempD1Ev({{.*}}[[TMP]])
 
 struct Temp2 {
@@ -291,7 +291,7 @@ void Temp2InArray() {
 // CIR-NEXT:     %[[CURRENT:.*]] = cir.load %[[ITER:.*]] : !cir.ptr<!cir.ptr<!rec_CausesTemp2>>, !cir.ptr<!rec_CausesTemp2>
 // CIR-NEXT:     cir.call @_ZN5Temp2C1Ev(%[[TMP]])
 // CIR-NEXT:     cir.cleanup.scope {
-// CIR-NEXT:         cir.call @_ZN11CausesTemp2C1E5Temp2(%[[CURRENT]], %[[TMP]]) : ({{.*}}, !cir.ptr<!rec_Temp2> {llvm.align = 1 : i64, llvm.byref = !rec_Temp2}) -> ()
+// CIR-NEXT:         cir.call @_ZN11CausesTemp2C1E5Temp2(%[[CURRENT]], %[[TMP]]) : ({{.*}}, !cir.ptr<!rec_Temp2> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR-NEXT:         cir.yield
 // CIR-NEXT:       } cleanup normal {
 // CIR-NEXT:         cir.call @_ZN5Temp2D1Ev(%[[TMP]]) nothrow
@@ -328,7 +328,7 @@ void Temp2InArray() {
 // LLVM:       call void @_ZN5Temp2C1Ev({{.*}}%[[TMP]])
 // LLVM:       br label %[[CONSTRUCT_BR:.*]]
 // LLVM:       [[CONSTRUCT_BR]]:
-// LLVM:       call void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}}%[[CURRENT]], ptr byref(%struct.Temp2) align 1 %[[TMP]])
+// LLVM:       call void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}}%[[CURRENT]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[TMP]])
 // LLVM:       br label %[[CLEANUP_BR:.*]]
 // LLVM:       [[CLEANUP_BR]]:
 // LLVM:       call void @_ZN5Temp2D1Ev({{.*}}[[TMP]])
@@ -341,7 +341,7 @@ void Temp2InArray() {
 // OGCG:       [[LOOP]]:
 // OGCG:       %[[CURRENT:.*]] = phi ptr
 // OGCG:       call void @_ZN5Temp2C1Ev(ptr {{.*}}%[[TMP]])
-// OGCG-NEXT:  call void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}}%[[CURRENT]], ptr{{.*}}[[TMP]])
+// OGCG-NEXT:  call void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}}%[[CURRENT]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[TMP]])
 // OGCG-NEXT:  call void @_ZN5Temp2D1Ev({{.*}}[[TMP]])
 // OGCG:       %[[DTOR_ELT:.*]] = getelementptr inbounds %struct.CausesTemp2, ptr %{{.*}}, i64 -1
 // OGCG-NEXT:  call void @_ZN11CausesTemp2D1Ev(ptr {{.*}}%[[DTOR_ELT]])
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-byref.cpp b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp
similarity index 65%
rename from clang/test/CIR/CodeGen/call-conv-lowering-x86_64-byref.cpp
rename to clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp
index 99f1ffd27a595..a01414569e6fa 100644
--- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-byref.cpp
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp
@@ -20,81 +20,77 @@ struct WithCopyCtor {
   WithCopyCtor(const WithCopyCtor &);
 };
 
-void takeByref(WithDtor t);
-void takeTwoByref(WithDtor a, WithDtor b);
+void takeNonByval(WithDtor t);
+void takeTwoNonByval(WithDtor a, WithDtor b);
 void takeByval(Big b);
-void takeCopyCtorByref(WithCopyCtor c);
+void takeCopyCtorNonByval(WithCopyCtor c);
 
 // The callee must receive the temporary the caller destroys, not a copy of it.
-void callByref() {
+void callNonByval() {
   WithDtor t;
-  takeByref(t);
+  takeNonByval(t);
 }
 
-// CIR-LABEL: cir.func {{.*}}@_Z9callByrefv
+// CIR-LABEL: cir.func {{.*}}@_Z12callNonByvalv
 // CIR:         %[[T:.*]] = cir.alloca "t" align(4) : !cir.ptr<!rec_WithDtor>
 // CIR:         %[[TMP:.*]] = cir.alloca "agg.tmp0" align(4) : !cir.ptr<!rec_WithDtor>
 // CIR:         cir.copy %[[T]] align(4) to %[[TMP]] align(4) : !cir.ptr<!rec_WithDtor>
 // CIR-NOT:     cir.load
-// CIR:         cir.call @_Z9takeByref8WithDtor(%[[TMP]]) : (!cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.byref = !rec_WithDtor}) -> ()
+// CIR:         cir.call @_Z12takeNonByval8WithDtor(%[[TMP]]) : (!cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR:         cir.call @_ZN8WithDtorD1Ev(%[[TMP]])
 // CIR:         cir.call @_ZN8WithDtorD1Ev(%[[T]])
 
-// LLVM-LABEL: define dso_local void @_Z9callByrefv()
+// LLVM-LABEL: define dso_local void @_Z12callNonByvalv()
 // LLVM:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[TMP:[^,]+]], ptr align 4 %[[T:[^,]+]], i64 4, i1 false)
-// CIR marks the byref argument and drops the ownership and dereferenceability
-// attrs classic emits, and classic adds dead_on_return on the destructor calls.
-// LLVM-CIR:     call void @_Z9takeByref8WithDtor(ptr byref(%struct.WithDtor) align 4 %[[TMP]])
+// LLVM:         call void @_Z12takeNonByval8WithDtor(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP]])
 // LLVM-CIR:     call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP]])
 // LLVM-CIR:     call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[T]])
-// OGCG:         call void @_Z9takeByref8WithDtor(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP]])
 // OGCG:         call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP]])
 // OGCG:         call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[T]])
 
-// Each byref argument forwards its own temporary.
-void callTwoByref() {
+// Each non-byval argument forwards its own temporary.
+void callTwoNonByval() {
   WithDtor a, b;
-  takeTwoByref(a, b);
+  takeTwoNonByval(a, b);
 }
 
-// CIR-LABEL: cir.func {{.*}}@_Z12callTwoByrefv
+// CIR-LABEL: cir.func {{.*}}@_Z15callTwoNonByvalv
 // CIR:         %[[TMP_A:.*]] = cir.alloca "agg.tmp0" align(4) : !cir.ptr<!rec_WithDtor>
 // CIR:         %[[TMP_B:.*]] = cir.alloca "agg.tmp1" align(4) : !cir.ptr<!rec_WithDtor>
 // CIR-NOT:     cir.load
-// CIR:         cir.call @_Z12takeTwoByref8WithDtorS_(%[[TMP_A]], %[[TMP_B]]) : (!cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.byref = !rec_WithDtor}, !cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.byref = !rec_WithDtor}) -> ()
+// CIR:         cir.call @_Z15takeTwoNonByval8WithDtorS_(%[[TMP_A]], %[[TMP_B]]) : (!cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef}, !cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR:         cir.call @_ZN8WithDtorD1Ev(%[[TMP_B]])
 // CIR:         cir.call @_ZN8WithDtorD1Ev(%[[TMP_A]])
 
-// LLVM-LABEL: define dso_local void @_Z12callTwoByrefv()
+// LLVM-LABEL: define dso_local void @_Z15callTwoNonByvalv()
 // LLVM:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[TMP_A:[^,]+]], ptr align 4 %{{[^,]+}}, i64 4, i1 false)
 // LLVM:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[TMP_B:[^,]+]], ptr align 4 %{{[^,]+}}, i64 4, i1 false)
-// LLVM-CIR:     call void @_Z12takeTwoByref8WithDtorS_(ptr byref(%struct.WithDtor) align 4 %[[TMP_A]], ptr byref(%struct.WithDtor) align 4 %[[TMP_B]])
+// LLVM:         call void @_Z15takeTwoNonByval8WithDtorS_(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP_A]], ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP_B]])
 // LLVM-CIR:     call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP_B]])
 // LLVM-CIR:     call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP_A]])
-// OGCG:         call void @_Z12takeTwoByref8WithDtorS_(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP_A]], ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP_B]])
 // OGCG:         call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP_B]])
 // OGCG:         call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP_A]])
 
-// A non-trivial copy constructor also classifies byref: the constructor call
-// populates the forwarded temporary directly, with no load in between.
-void callCopyCtorByref() {
+// A non-trivial copy constructor also classifies non-byval: the constructor
+// call populates the forwarded temporary directly, with no load in between.
+void callCopyCtorNonByval() {
   WithCopyCtor c;
-  takeCopyCtorByref(c);
+  takeCopyCtorNonByval(c);
 }
 
-// CIR-LABEL: cir.func {{.*}}@_Z17callCopyCtorByrefv
+// CIR-LABEL: cir.func {{.*}}@_Z20callCopyCtorNonByvalv
 // CIR:         %[[C:.*]] = cir.alloca "c" align(4) init : !cir.ptr<!rec_WithCopyCtor>
 // CIR:         %[[TMP:.*]] = cir.alloca "agg.tmp0" align(4) : !cir.ptr<!rec_WithCopyCtor>
 // CIR:         cir.call @_ZN12WithCopyCtorC1Ev(%[[C]])
 // CIR:         cir.call @_ZN12WithCopyCtorC1ERKS_(%[[TMP]], %[[C]])
 // CIR-NOT:     cir.load
-// CIR:         cir.call @_Z17takeCopyCtorByref12WithCopyCtor(%[[TMP]]) : (!cir.ptr<!rec_WithCopyCtor> {llvm.align = 4 : i64, llvm.byref = !rec_WithCopyCtor}) -> ()
+// CIR:         cir.call @_Z20takeCopyCtorNonByval12WithCopyCtor(%[[TMP]]) : (!cir.ptr<!rec_WithCopyCtor> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
-// LLVM-LABEL: define dso_local void @_Z17callCopyCtorByrefv()
+// LLVM-LABEL: define dso_local void @_Z20callCopyCtorNonByvalv()
 // LLVM:         call void @_ZN12WithCopyCtorC1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[C:[^)]+]])
 // LLVM:         call void @_ZN12WithCopyCtorC1ERKS_(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP:[^,]+]], ptr noundef nonnull align 4 dereferenceable(4) %[[C]])
-// LLVM-CIR:     call void @_Z17takeCopyCtorByref12WithCopyCtor(ptr byref(%struct.WithCopyCtor) align 4 %[[TMP]])
-// OGCG:         call void @_Z17takeCopyCtorByref12WithCopyCtor(ptr nofreeobj noundef align 4 dead_on_return dereferenceable(4) %[[TMP]])
+// LLVM-CIR:     call void @_Z20takeCopyCtorNonByval12WithCopyCtor(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP]])
+// OGCG:         call void @_Z20takeCopyCtorNonByval12WithCopyCtor(ptr nofreeobj noundef align 4 dead_on_return dereferenceable(4) %[[TMP]])
 
 // byval keeps the fresh copy the callee owns.
 void callByval() {
@@ -129,33 +125,31 @@ void callInheritedCtor(WithDtor t) { Derived d(t); }
 // CIR:         %[[TMP:.*]] = cir.alloca "agg.tmp0" align(4) : !cir.ptr<!rec_WithDtor>
 // CIR:         cir.copy %{{.*}} to %[[TMP]]
 // CIR:         cir.call @_ZN7DerivedCI14BaseE8WithDtor(%{{.*}}, %[[TMP]])
-// CIR-SAME:      llvm.byref = !rec_WithDtor
+// CIR-SAME:      llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef
 
-// LLVM-CIR:     define dso_local void @_Z17callInheritedCtor8WithDtor(ptr byref(%struct.WithDtor) align 4 %[[INHARG:[0-9]+]])
-// OGCG:         define dso_local void @_Z17callInheritedCtor8WithDtor(ptr nofreeobj noundef align 4 dereferenceable(4) %[[INHARG:[^)]+]])
+// LLVM:         define dso_local void @_Z17callInheritedCtor8WithDtor(ptr nofreeobj noundef align 4 dereferenceable(4) %[[INHARG:[^,)]+]])
 // LLVM:          %[[INHTMP:.+]] = alloca %struct.WithDtor, align 4
 // LLVM:          call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[INHTMP]], ptr align 4 %[[INHARG]], i64 4, i1 false)
-// LLVM-CIR:      call void @_ZN7DerivedCI14BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{.+}}, ptr byref(%struct.WithDtor) align 4 %[[INHTMP]])
-// OGCG:          call void @_ZN7DerivedCI14BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{.+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[INHTMP]])
+// LLVM:          call void @_ZN7DerivedCI14BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{.+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[INHTMP]])
 // LLVM-CIR:      call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[INHTMP]])
 // OGCG:          call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[INHTMP]])
 
 // Both inheriting constructor variants hand their own parameter on unchanged.
 // CIR-LABEL: cir.func {{.*}}@_ZN7DerivedCI14BaseE8WithDtor
-// CIR-SAME:      %[[CI1ARG:[^:]*]]: !cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.byref = !rec_WithDtor}
+// CIR-SAME:      %[[CI1ARG:[^:]*]]: !cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef}
 // CIR-NOT:     cir.copy
 // CIR:         cir.call @_ZN7DerivedCI24BaseE8WithDtor(%{{.*}}, %[[CI1ARG]])
 
 // CIR-LABEL: cir.func {{.*}}@_ZN7DerivedCI24BaseE8WithDtor
-// CIR-SAME:      %[[ARG:[^:]*]]: !cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.byref = !rec_WithDtor}
+// CIR-SAME:      %[[ARG:[^:]*]]: !cir.ptr<!rec_WithDtor> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef}
 // CIR-NOT:     cir.copy
 // CIR:         cir.call @_ZN4BaseC2E8WithDtor(%{{.*}}, %[[ARG]])
 
-// LLVM-CIR:     define {{.*}}@_ZN7DerivedCI14BaseE8WithDtor(ptr{{.*}}, ptr byref(%struct.WithDtor) align 4 %[[CI1ARG:[0-9]+]])
-// LLVM-CIR-NOT: alloca %struct.WithDtor
-// LLVM-CIR:     call void @_ZN7DerivedCI24BaseE8WithDtor(ptr{{.*}}, ptr byref(%struct.WithDtor) align 4 %[[CI1ARG]])
-// LLVM-CIR:     define {{.*}}@_ZN7DerivedCI24BaseE8WithDtor(ptr{{.*}}, ptr byref(%struct.WithDtor) align 4 %[[ARG:[0-9]+]])
-// LLVM-CIR-NOT: alloca %struct.WithDtor
-// LLVM-CIR:     call void @_ZN4BaseC2E8WithDtor(ptr{{.*}}, ptr byref(%struct.WithDtor) align 4 %[[ARG]])
-// OGCG:         define {{.*}}@_ZN7DerivedCI24BaseE8WithDtor(ptr{{.*}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[ARG:[0-9]+]])
-// OGCG:         call void @_ZN4BaseC2E8WithDtor(ptr{{.*}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[ARG]])
+// LLVM-CIR:     define {{.*}}@_ZN7DerivedCI14BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{[^,]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[CI1ARG:[^,)]+]])
+// OGCG:         define {{.*}}@_ZN7DerivedCI14BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{[^,]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[CI1ARG:[^,)]+]])
+// LLVM-NOT:     alloca %struct.WithDtor
+// LLVM:         call void @_ZN7DerivedCI24BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{[^,]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[CI1ARG]])
+// LLVM-CIR:     define {{.*}}@_ZN7DerivedCI24BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{[^,]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[ARG:[^,)]+]])
+// OGCG:         define {{.*}}@_ZN7DerivedCI24BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{[^,]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[ARG:[^,)]+]])
+// LLVM-NOT:     alloca %struct.WithDtor
+// LLVM:         call void @_ZN4BaseC2E8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{[^,]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[ARG]])
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp
index 24befc755b6d5..e55fca949bc14 100644
--- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-vptr.cpp
@@ -20,53 +20,53 @@ struct VirtInherit : virtual VBase { long b; };
 // passed by invisible reference whatever its eightbytes classify as.
 int takePoly(Poly v, int k) { return k; }
 
-// CIR: cir.func {{.*}}@_Z8takePoly4Polyi(%arg0: !cir.ptr<!rec_Poly> {llvm.align = 8 : i64, llvm.byref = !rec_Poly}{{.*}}, %arg1: !s32i {{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z8takePoly4Polyi(ptr byref(%struct.Poly) align 8 %{{[^,]+}}, i32 noundef %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z8takePoly4Polyi(%arg0: !cir.ptr<!rec_Poly> {llvm.align = 8 : i64, llvm.dereferenceable = 8 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}, %arg1: !s32i {{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z8takePoly4Polyi(ptr nofreeobj noundef align 8 dereferenceable(8) %{{[^,]+}}, i32 noundef %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z8takePoly4Polyi(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(8) %{{[^,]+}}, i32 noundef %{{[^,)]+}})
 
 int takePolyLong(PolyLong v) { return 0; }
 
-// CIR: cir.func {{.*}}@_Z12takePolyLong8PolyLong(%arg0: !cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.byref = !rec_PolyLong}{{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z12takePolyLong8PolyLong(ptr byref(%struct.PolyLong) align 8 %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z12takePolyLong8PolyLong(%arg0: !cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z12takePolyLong8PolyLong(ptr nofreeobj noundef align 8 dereferenceable(16) %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z12takePolyLong8PolyLong(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
 
 // The copy constructor decides this, so it does not matter what the members
 // would have classified as on their own.
 int takePolyTwoInt(PolyTwoInt v) { return v.y; }
 
-// CIR: cir.func {{.*}}@_Z14takePolyTwoInt10PolyTwoInt(%arg0: !cir.ptr<!rec_PolyTwoInt> {llvm.align = 8 : i64, llvm.byref = !rec_PolyTwoInt}{{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z14takePolyTwoInt10PolyTwoInt(ptr byref(%struct.PolyTwoInt) align 8 %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z14takePolyTwoInt10PolyTwoInt(%arg0: !cir.ptr<!rec_PolyTwoInt> {llvm.align = 8 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z14takePolyTwoInt10PolyTwoInt(ptr nofreeobj noundef align 8 dereferenceable(16) %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z14takePolyTwoInt10PolyTwoInt(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
 
 // Past two eightbytes SysV says memory on its own, so the two rules agree here.
 int takePolyBig(PolyBig v) { return 0; }
 
-// CIR: cir.func {{.*}}@_Z11takePolyBig7PolyBig(%arg0: !cir.ptr<!rec_PolyBig> {llvm.align = 8 : i64, llvm.byref = !rec_PolyBig}{{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z11takePolyBig7PolyBig(ptr byref(%struct.PolyBig) align 8 %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z11takePolyBig7PolyBig(%arg0: !cir.ptr<!rec_PolyBig> {llvm.align = 8 : i64, llvm.dereferenceable = 24 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z11takePolyBig7PolyBig(ptr nofreeobj noundef align 8 dereferenceable(24) %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z11takePolyBig7PolyBig(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(24) %{{[^,)]+}})
 
 // The vtable pointer is inherited through the base subobject rather than
 // declared here.
 int takePolyDerived(PolyDerived v) { return 0; }
 
-// CIR: cir.func {{.*}}@_Z15takePolyDerived11PolyDerived(%arg0: !cir.ptr<!rec_PolyDerived> {llvm.align = 8 : i64, llvm.byref = !rec_PolyDerived}{{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z15takePolyDerived11PolyDerived(ptr byref(%struct.PolyDerived) align 8 %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z15takePolyDerived11PolyDerived(%arg0: !cir.ptr<!rec_PolyDerived> {llvm.align = 8 : i64, llvm.dereferenceable = 24 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z15takePolyDerived11PolyDerived(ptr nofreeobj noundef align 8 dereferenceable(24) %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z15takePolyDerived11PolyDerived(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(24) %{{[^,)]+}})
 
 // HasPoly declares no virtual function of its own, but its member carries the
 // vtable pointer and the non-trivial copy constructor with it.
 int takeHasPoly(HasPoly v) { return 0; }
 
-// CIR: cir.func {{.*}}@_Z11takeHasPoly7HasPoly(%arg0: !cir.ptr<!rec_HasPoly> {llvm.align = 8 : i64, llvm.byref = !rec_HasPoly}{{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z11takeHasPoly7HasPoly(ptr byref(%struct.HasPoly) align 8 %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z11takeHasPoly7HasPoly(%arg0: !cir.ptr<!rec_HasPoly> {llvm.align = 8 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z11takeHasPoly7HasPoly(ptr nofreeobj noundef align 8 dereferenceable(16) %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z11takeHasPoly7HasPoly(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
 
 // A virtual base gives the class a vtable pointer for the base offset even
 // though it declares no virtual function of its own.
 int takeVirtInherit(VirtInherit v) { return 0; }
 
-// CIR: cir.func {{.*}}@_Z15takeVirtInherit11VirtInherit(%arg0: !cir.ptr<!rec_VirtInherit> {llvm.align = 8 : i64, llvm.byref = !rec_VirtInherit}{{.*}}) -> (!s32i
-// LLVM-CIR: define dso_local noundef i32 @_Z15takeVirtInherit11VirtInherit(ptr byref(%struct.VirtInherit) align 8 %{{[^,)]+}})
+// CIR: cir.func {{.*}}@_Z15takeVirtInherit11VirtInherit(%arg0: !cir.ptr<!rec_VirtInherit> {llvm.align = 8 : i64, llvm.dereferenceable = 24 : i64, llvm.nofreeobj, llvm.noundef}{{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z15takeVirtInherit11VirtInherit(ptr nofreeobj noundef align 8 dereferenceable(24) %{{[^,)]+}})
 // LLVM-OGCG: define dso_local noundef i32 @_Z15takeVirtInherit11VirtInherit(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(24) %{{[^,)]+}})
 
 // Returning the class writes through an sret slot the caller supplies.
@@ -87,10 +87,10 @@ int caller(int k) {
 
 // CIR: cir.func {{.*}}@_Z6calleri(%arg0: !s32i {{.*}}) -> (!s32i
 // CIR:   cir.call @_Z11retPolyLongv(%{{[0-9]+}}) : (!cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.dead_on_unwind, llvm.sret = !rec_PolyLong, llvm.writable}) -> ()
-// CIR:   cir.call @_Z12takePolyLong8PolyLong(%{{[0-9]+}}) : (!cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.byref = !rec_PolyLong}) -> (!s32i
+// CIR:   cir.call @_Z12takePolyLong8PolyLong(%{{[0-9]+}}) : (!cir.ptr<!rec_PolyLong> {llvm.align = 8 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef}) -> (!s32i
 // LLVM: define dso_local noundef i32 @_Z6calleri(i32 noundef %{{[^,)]+}})
 // LLVM:   call void @_Z11retPolyLongv(ptr dead_on_unwind writable sret(%struct.PolyLong) align 8 %{{[^,)]+}})
-// LLVM-CIR:   call noundef i32 @_Z12takePolyLong8PolyLong(ptr byref(%struct.PolyLong) align 8 %{{[^,)]+}})
+// LLVM-CIR:   call noundef i32 @_Z12takePolyLong8PolyLong(ptr nofreeobj noundef align 8 dereferenceable(16) %{{[^,)]+}})
 // LLVM-OGCG:   call noundef i32 @_Z12takePolyLong8PolyLong(ptr nofreeobj noundef align 8 dead_on_return dereferenceable(16) %{{[^,)]+}})
 
 // Returned, the class uses sret at its declared alignment.
diff --git a/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper-eh.cpp b/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper-eh.cpp
index 5792c78cfad49..323ced9c2d521 100644
--- a/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper-eh.cpp
+++ b/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper-eh.cpp
@@ -51,7 +51,7 @@ Wrapper makeWrapper() {
 // CIR:       cir.call @_ZNSt10unique_ptrI4BaseEC1EPS0_(%[[AGG_TMP0]], %[[SOURCE]])
 // CIR:       %[[TRUE:.*]] = cir.const #true
 // CIR:       cir.store %[[TRUE]], %[[CLEANUP_COND]]
-// CIR:       cir.call @_ZN7WrapperC1ESt10unique_ptrI4BaseE(%[[RETVAL]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_std3A3Aunique_ptr3CBase3E> {llvm.align = 1 : i64, llvm.byref = !rec_std3A3Aunique_ptr3CBase3E}) -> ()
+// CIR:       cir.call @_ZN7WrapperC1ESt10unique_ptrI4BaseE(%[[RETVAL]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_std3A3Aunique_ptr3CBase3E> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR:     } else {
 // CIR:       cir.call @_ZN7Wrapper5emptyEv(%[[RETVAL]])
 // CIR:     }
@@ -89,7 +89,7 @@ Wrapper makeWrapper() {
 // LLVM:                       to label %[[INVOKE_CONTINUE_2:.*]] unwind label %[[INVOKE_CLEANUP:.*]]
 // LLVM: [[INVOKE_CONTINUE_2]]:
 // LLVM:   store i8 1, ptr %[[CLEANUP_COND]]
-// LLVM:   invoke void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr byref(%"struct.std::unique_ptr<Base>") align 1 %[[AGG_TMP0]])
+// LLVM:   invoke void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP0]])
 // LLVM:                       to label %[[INVOKE_CONTINUE_3:.*]] unwind label %[[INVOKE_CLEANUP:.*]]
 // LLVM: [[INVOKE_CONTINUE_3]]:
 // LLVM:   br label %[[CONSTRUCT_CONTINUE:.*]]
@@ -136,7 +136,7 @@ Wrapper makeWrapper() {
 // OGCG:   %[[SOURCE:.*]] = call {{.*}} ptr @_Z9getSourcev()
 // OGCG:   call void @_ZNSt10unique_ptrI4BaseEC1EPS0_(ptr {{.*}} %[[AGG_TMP]], {{.*}} %[[SOURCE]])
 // OGCG:   store i1 true, ptr %[[CLEANUP_COND]]
-// OGCG:   invoke void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr {{.*}} %[[AGG_TMP]])
+// OGCG:   invoke void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP]])
 // OGCG:           to label %[[INVOKE_CONTINUE:.*]] unwind label %[[INVOKE_CLEANUP:.*]]
 // OGCG: [[INVOKE_CONTINUE]]:
 // OGCG:   br label %[[COND_END:.*]]
@@ -361,7 +361,7 @@ void makeEntry() {
 // CIR:         %{{.*}} = cir.get_global @g_path
 // CIR:         %[[TRUE:.*]] = cir.const #true
 // CIR:         cir.store %[[TRUE]], %[[CLEANUP_COND]]
-// CIR:         cir.call @_ZN5EntryC1E4Path(%[[ENSURED_F]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_Path> {llvm.align = 1 : i64, llvm.byref = !rec_Path}) -> ()
+// CIR:         cir.call @_ZN5EntryC1E4Path(%[[ENSURED_F]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_Path> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR:       }
 // CIR:       cir.yield
 // CIR:     } cleanup all {
@@ -395,7 +395,7 @@ void makeEntry() {
 // LLVM:   br label %[[COND_END:.*]]
 // LLVM: [[FALSE_BB]]:
 // LLVM:   store i8 1, ptr %[[CLEANUP_COND]]
-// LLVM:   invoke void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr byref(%struct.Path) align 1 %[[AGG_TMP0]])
+// LLVM:   invoke void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP0]])
 // LLVM:                     to label %[[FALSE_CONT:.*]] unwind label %[[LPAD]]
 // LLVM: [[FALSE_CONT]]:
 // LLVM:   br label %[[COND_END]]
@@ -455,7 +455,7 @@ void makeEntry() {
 // OGCG:   br label %[[COND_END:.*]]
 // OGCG: [[COND_FALSE]]:
 // OGCG:   store i1 true, ptr %[[CLEANUP_COND]]
-// OGCG:   invoke void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr {{.*}} %[[AGG_TMP]])
+// OGCG:   invoke void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP]])
 // OGCG:           to label %[[FALSE_CONT:.*]] unwind label %[[LPAD2:.*]]
 // OGCG: [[FALSE_CONT]]:
 // OGCG:   br label %[[COND_END]]
diff --git a/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper.cpp b/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper.cpp
index 19aaeff91199b..825172ad5b43c 100644
--- a/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper.cpp
+++ b/clang/test/CIR/CodeGen/cleanup-conditional-with-wrapper.cpp
@@ -51,7 +51,7 @@ Wrapper makeWrapper() {
 // CIR:       cir.call @_ZNSt10unique_ptrI4BaseEC1EPS0_(%[[AGG_TMP0]], %[[SOURCE]])
 // CIR:       %[[TRUE:.*]] = cir.const #true
 // CIR:       cir.store %[[TRUE]], %[[CLEANUP_COND]]
-// CIR:       cir.call @_ZN7WrapperC1ESt10unique_ptrI4BaseE(%[[RETVAL]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_std3A3Aunique_ptr3CBase3E> {llvm.align = 1 : i64, llvm.byref = !rec_std3A3Aunique_ptr3CBase3E}) -> ()
+// CIR:       cir.call @_ZN7WrapperC1ESt10unique_ptrI4BaseE(%[[RETVAL]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_std3A3Aunique_ptr3CBase3E> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR:     } else {
 // CIR:       cir.call @_ZN7Wrapper5emptyEv(%[[RETVAL]])
 // CIR:     }
@@ -75,7 +75,7 @@ Wrapper makeWrapper() {
 // LLVM:   %[[SOURCE:.*]] = call {{.*}} ptr @_Z9getSourcev()
 // LLVM:   call void @_ZNSt10unique_ptrI4BaseEC1EPS0_(ptr {{.*}} %[[AGG_TMP0]], ptr {{.*}} %[[SOURCE]])
 // LLVM:   store i8 1, ptr %[[CLEANUP_COND]]
-// LLVM:   call void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr byref(%"struct.std::unique_ptr<Base>") align 1 %[[AGG_TMP0]])
+// LLVM:   call void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP0]])
 // LLVM:   br label %[[CONSTRUCT_CONTINUE:.*]]
 // LLVM: [[CONSTRUCT_FALSE]]:
 // LLVM:   call void @_ZN7Wrapper5emptyEv(ptr {{.*}} sret(%struct.Wrapper) {{.*}} %[[RETVAL]])
@@ -103,7 +103,7 @@ Wrapper makeWrapper() {
 // OGCG:   %[[SOURCE:.*]] = call {{.*}} ptr @_Z9getSourcev()
 // OGCG:   call void @_ZNSt10unique_ptrI4BaseEC1EPS0_(ptr {{.*}} %[[AGG_TMP]], {{.*}} %[[SOURCE]])
 // OGCG:   store i1 true, ptr %[[CLEANUP_COND]]
-// OGCG:   call void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr {{.*}} %[[AGG_TMP]])
+// OGCG:   call void @_ZN7WrapperC1ESt10unique_ptrI4BaseE(ptr {{.*}} %[[RETVAL]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP]])
 // OGCG:   br label %[[COND_END:.*]]
 // OGCG: [[COND_FALSE]]:
 // OGCG:   call void @_ZN7Wrapper5emptyEv(ptr {{.*}} %[[RETVAL]])
@@ -282,7 +282,7 @@ void makeEntry() {
 // CIR:         %{{.*}} = cir.get_global @g_path
 // CIR:         %[[TRUE:.*]] = cir.const #true
 // CIR:         cir.store %[[TRUE]], %[[CLEANUP_COND]]
-// CIR:         cir.call @_ZN5EntryC1E4Path(%[[ENSURED_F]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_Path> {llvm.align = 1 : i64, llvm.byref = !rec_Path}) -> ()
+// CIR:         cir.call @_ZN5EntryC1E4Path(%[[ENSURED_F]], %[[AGG_TMP0]]) : ({{.*}}, !cir.ptr<!rec_Path> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR:       }
 // CIR:       cir.yield
 // CIR:     } cleanup normal {
@@ -314,7 +314,7 @@ void makeEntry() {
 // LLVM:   br label %[[COND_END:.*]]
 // LLVM: [[FALSE_BB]]:
 // LLVM:   store i8 1, ptr %[[CLEANUP_COND]]
-// LLVM:   call void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr byref(%struct.Path) align 1 %[[AGG_TMP0]])
+// LLVM:   call void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP0]])
 // LLVM:   br label %[[COND_END]]
 // LLVM: [[COND_END]]:
 // LLVM:   br label %[[AFTER_INNER:.*]]
@@ -345,7 +345,7 @@ void makeEntry() {
 // OGCG:   br label %[[COND_END:.*]]
 // OGCG: [[COND_FALSE]]:
 // OGCG:   store i1 true, ptr %[[CLEANUP_COND]]
-// OGCG:   call void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr {{.*}} %[[AGG_TMP]])
+// OGCG:   call void @_ZN5EntryC1E4Path(ptr {{.*}} %[[ENSURED_F]], ptr nofreeobj noundef align 1 dereferenceable(1) %[[AGG_TMP]])
 // OGCG:   br label %[[COND_END]]
 // OGCG: [[COND_END]]:
 // OGCG:   %[[IS_ACTIVE:.*]] = load i1, ptr %[[CLEANUP_COND]]
diff --git a/clang/test/CIR/CodeGen/lambda-dtor-field.cpp b/clang/test/CIR/CodeGen/lambda-dtor-field.cpp
index 0d96ca433553b..e6ef50c57dc14 100644
--- a/clang/test/CIR/CodeGen/lambda-dtor-field.cpp
+++ b/clang/test/CIR/CodeGen/lambda-dtor-field.cpp
@@ -28,17 +28,15 @@ void capture_one(S s) {
 // CIR:           cir.yield
 // CIR:         }
 
-// TODO(cir): CIR marks the indirect parameter byref and drops noundef where
-// classic CodeGen emits a plain noundef pointer.
 // LLVM-LABEL: define dso_local void @_Z11capture_one1S(
-// LLVM-SAME:    ptr byref(%struct.S) align 4 %[[S_ARG:[^,)]+]])
+// LLVM-SAME:    ptr nofreeobj noundef align 4 dereferenceable(4) %[[S_ARG:[^,)]+]])
 // LLVM:   %[[LAM1:.*]] = alloca %[[LAM_TY_1:[^,]*]]
 // LLVM:   %[[F1:.*]] = getelementptr inbounds nuw %[[LAM_TY_1]], ptr %[[LAM1]], i32 0, i32 0
 // LLVM:   call void @_ZN1SC1ERKS_(ptr {{.*}} %[[F1]], ptr {{.*}} %[[S_ARG]])
 // LLVM:   call void @"_ZZ11capture_one1SEN3$_0D1Ev"(ptr {{.*}} %[[LAM1]])
 // LLVM:   ret void
 
-// OGCG-LABEL: define dso_local void @_Z11capture_one1S(ptr{{.*}} align 4 {{.*}}%{{[^,)]+}})
+// OGCG-LABEL: define dso_local void @_Z11capture_one1S(ptr nofreeobj noundef align 4 dereferenceable(4) %{{[^,)]+}})
 // OGCG:   %[[LAM1:.*]] = alloca %[[LAM_TY_1:.*]], align 4
 // OGCG:   %[[FIELD1:.*]] = getelementptr inbounds nuw %[[LAM_TY_1]], ptr %[[LAM1]], i32 0, i32 0
 // OGCG:   call void @_ZN1SC1ERKS_(ptr {{.*}} %[[FIELD1]], ptr {{.*}} %s)
@@ -69,7 +67,7 @@ void capture_two(S a, S b) {
 // CIR:         }
 
 // LLVM-LABEL: define dso_local void @_Z11capture_two1SS_(
-// LLVM-SAME:    ptr byref(%struct.S) align 4 %[[A_ARG:[^,)]+]], ptr byref(%struct.S) align 4 %[[B_ARG:[^,)]+]]) #{{.*}} personality ptr @__gxx_personality_v0 {
+// LLVM-SAME:    ptr nofreeobj noundef align 4 dereferenceable(4) %[[A_ARG:[^,)]+]], ptr nofreeobj noundef align 4 dereferenceable(4) %[[B_ARG:[^,)]+]]) #{{.*}} personality ptr @__gxx_personality_v0 {
 // LLVM:   %[[LAM2:.*]] = alloca %[[LAM_TY_2:[^,]*]]
 // LLVM:   %[[FA:.*]] = getelementptr inbounds nuw %[[LAM_TY_2]], ptr %[[LAM2]], i32 0, i32 0
 // LLVM:   call void @_ZN1SC1ERKS_(ptr {{.*}} %[[FA]], ptr {{.*}} %[[A_ARG]])
@@ -108,7 +106,7 @@ void capture_mixed(int n, S s) {
 // CIR:         }
 
 // LLVM-LABEL: define dso_local void @_Z13capture_mixedi1S(
-// LLVM-SAME:    i32 {{[^,)]*}} %{{[^,)]+}}, ptr byref(%struct.S) align 4 %[[S_ARG2:[^,)]+]])
+// LLVM-SAME:    i32 {{[^,)]*}} %{{[^,)]+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[S_ARG2:[^,)]+]])
 // LLVM:   %[[N_ALLOCA:.*]] = alloca i32
 // LLVM:   %[[LAM3:.*]] = alloca %[[LAM_TY_3:[^,]*]]
 // LLVM:   %[[FN:.*]] = getelementptr inbounds nuw %[[LAM_TY_3]], ptr %[[LAM3]], i32 0, i32 0
diff --git a/clang/test/CIR/CodeGen/partial-array-cleanup.cpp b/clang/test/CIR/CodeGen/partial-array-cleanup.cpp
index 6ae7e19ba3bdb..3cdf30906f36e 100644
--- a/clang/test/CIR/CodeGen/partial-array-cleanup.cpp
+++ b/clang/test/CIR/CodeGen/partial-array-cleanup.cpp
@@ -1237,7 +1237,7 @@ void Temp2InArray() {
 // CIR-NEXT:      %[[CURRENT:.*]] = cir.load %[[ARR_IDX]] : !cir.ptr<!cir.ptr<!rec_CausesTemp2>>, !cir.ptr<!rec_CausesTemp2>
 // CIR-NEXT:      cir.call @_ZN5Temp2C1Ev(%[[TMP]])
 // CIR-NEXT:      cir.cleanup.scope {
-// CIR-NEXT:        cir.call @_ZN11CausesTemp2C1E5Temp2(%[[CURRENT]], %[[TMP]]) : ({{.*}}, !cir.ptr<!rec_Temp2> {llvm.align = 1 : i64, llvm.byref = !rec_Temp2}) -> ()
+// CIR-NEXT:        cir.call @_ZN11CausesTemp2C1E5Temp2(%[[CURRENT]], %[[TMP]]) : ({{.*}}, !cir.ptr<!rec_Temp2> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 // CIR-NEXT:        cir.yield
 // CIR-NEXT:      } cleanup all {
 // CIR-NEXT:        cir.call @_ZN5Temp2D1Ev(%[[TMP]]) nothrow
@@ -1287,7 +1287,7 @@ void Temp2InArray() {
 // LLVM: [[EMPTY2]]:
 // LLVM: br label %[[CONSTRUCT_CT:.*]]
 // LLVM: [[CONSTRUCT_CT]]:
-// LLVM: invoke void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}} %{{.*}}, ptr byref(%struct.Temp2) align 1 %[[TMP]])
+// LLVM: invoke void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}} %{{.*}}, ptr nofreeobj noundef align 1 dereferenceable(1) %[[TMP]])
 // LLVM-NEXT:         to label %[[EMPTY3:.*]] unwind label %[[EXCEPT:.*]]
 // LLVM: [[EMPTY3]]:
 // LLVM: br label %[[DTOR_TEMP:.*]]
@@ -1320,7 +1320,7 @@ void Temp2InArray() {
 // OGCG-NEXT:         to label %[[TMP_CTD:.*]] unwind label %[[TMP_UNWIND:.*]]
 
 // OGCG: [[TMP_CTD]]:
-// OGCG-NEXT: invoke void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}}%{{.*}}, ptr {{.*}}%[[TMP]])
+// OGCG-NEXT: invoke void @_ZN11CausesTemp2C1E5Temp2(ptr {{.*}}%{{.*}}, ptr nofreeobj noundef align 1 dereferenceable(1) %[[TMP]])
 // OGCG-NEXT:      to label %[[CTD:.*]] unwind label %[[UNWIND:.*]]
 
 // OGCG: [[CTD]]:
diff --git a/clang/test/CIR/CodeGenOpenACC/declare-copy.cpp b/clang/test/CIR/CodeGenOpenACC/declare-copy.cpp
index bffaa641bc20b..49e83b763f521 100644
--- a/clang/test/CIR/CodeGenOpenACC/declare-copy.cpp
+++ b/clang/test/CIR/CodeGenOpenACC/declare-copy.cpp
@@ -11,7 +11,7 @@ struct Struct {
   static const int StaticMemInt;
 
   void MemFunc1(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca "this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -73,7 +73,7 @@ void use() {
 }
 
 void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca "this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -142,7 +142,7 @@ void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEP
 extern "C" void do_thing();
 
 extern "C" void NormalFunc(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
     // CHECK-NEXT: %[[LOC_HSE_ALLOCA:.*]] = cir.alloca "LocalHSE" {{.*}} : !cir.ptr<!rec_HasSideEffects>
diff --git a/clang/test/CIR/CodeGenOpenACC/declare-copyin.cpp b/clang/test/CIR/CodeGenOpenACC/declare-copyin.cpp
index f4517b85cba58..5d2d27f472b98 100644
--- a/clang/test/CIR/CodeGenOpenACC/declare-copyin.cpp
+++ b/clang/test/CIR/CodeGenOpenACC/declare-copyin.cpp
@@ -259,7 +259,7 @@ struct Struct {
 // CHECK-NEXT: }
 
   void MemFunc1(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -321,7 +321,7 @@ void use() {
 }
 
 void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -390,7 +390,7 @@ void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEP
 extern "C" void do_thing();
 
 extern "C" void NormalFunc(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
     // CHECK-NEXT: %[[LOC_HSE_ALLOCA:.*]] = cir.alloca "LocalHSE" {{.*}} : !cir.ptr<!rec_HasSideEffects>
diff --git a/clang/test/CIR/CodeGenOpenACC/declare-copyout.cpp b/clang/test/CIR/CodeGenOpenACC/declare-copyout.cpp
index 73ea87736dbfa..00dafcd95371b 100644
--- a/clang/test/CIR/CodeGenOpenACC/declare-copyout.cpp
+++ b/clang/test/CIR/CodeGenOpenACC/declare-copyout.cpp
@@ -11,7 +11,7 @@ struct Struct {
   static const int StaticMemInt;
 
   void MemFunc1(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -73,7 +73,7 @@ void use() {
 }
 
 void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -142,7 +142,7 @@ void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEP
 extern "C" void do_thing();
 
 extern "C" void NormalFunc(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
     // CHECK-NEXT: %[[LOC_HSE_ALLOCA:.*]] = cir.alloca "LocalHSE" {{.*}} : !cir.ptr<!rec_HasSideEffects>
diff --git a/clang/test/CIR/CodeGenOpenACC/declare-create.cpp b/clang/test/CIR/CodeGenOpenACC/declare-create.cpp
index 124fcdc581814..d55a60519a4ce 100644
--- a/clang/test/CIR/CodeGenOpenACC/declare-create.cpp
+++ b/clang/test/CIR/CodeGenOpenACC/declare-create.cpp
@@ -260,7 +260,7 @@ struct Struct {
 // CHECK-NEXT: }
 
   void MemFunc1(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -322,7 +322,7 @@ void use() {
 }
 
 void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -391,7 +391,7 @@ void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEP
 extern "C" void do_thing();
 
 extern "C" void NormalFunc(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
     // CHECK-NEXT: %[[LOC_HSE_ALLOCA:.*]] = cir.alloca "LocalHSE" {{.*}} : !cir.ptr<!rec_HasSideEffects>
diff --git a/clang/test/CIR/CodeGenOpenACC/declare-deviceresident.cpp b/clang/test/CIR/CodeGenOpenACC/declare-deviceresident.cpp
index a9252cbaf3bff..9cda9d8bec285 100644
--- a/clang/test/CIR/CodeGenOpenACC/declare-deviceresident.cpp
+++ b/clang/test/CIR/CodeGenOpenACC/declare-deviceresident.cpp
@@ -259,7 +259,7 @@ struct Struct {
 // CHECK-NEXT: }
 
   void MemFunc1(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -321,7 +321,7 @@ void use() {
 }
 
 void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -390,7 +390,7 @@ void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEP
 extern "C" void do_thing();
 
 extern "C" void NormalFunc(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
     // CHECK-NEXT: %[[LOC_HSE_ALLOCA:.*]] = cir.alloca "LocalHSE" {{.*}} : !cir.ptr<!rec_HasSideEffects>
diff --git a/clang/test/CIR/CodeGenOpenACC/declare-present.cpp b/clang/test/CIR/CodeGenOpenACC/declare-present.cpp
index 4722caa5507c6..b54dbe7b2b63a 100644
--- a/clang/test/CIR/CodeGenOpenACC/declare-present.cpp
+++ b/clang/test/CIR/CodeGenOpenACC/declare-present.cpp
@@ -12,7 +12,7 @@ struct Struct {
   static const int StaticMemInt;
 
   void MemFunc1(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc1{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: cir.alloca "this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -74,7 +74,7 @@ void use() {
 }
 
 void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}MemFunc2{{.*}}(%{{.*}}: !cir.ptr<!rec_Struct>{{.*}}, %[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
 // CHECK-NEXT: cir.alloca{{.*}}"this"
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
@@ -143,7 +143,7 @@ void Struct::MemFunc2(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEP
 extern "C" void do_thing();
 
 extern "C" void NormalFunc(HasSideEffects ArgHSE, int ArgInt, HasSideEffects *ArgHSEPtr) {
-    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.byref = !rec_HasSideEffects{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
+    // CHECK: cir.func {{.*}}NormalFunc(%[[ARG_HSE:.*]]: !cir.ptr<!rec_HasSideEffects> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef{{.*}}, %[[ARG_INT:.*]]: !s32i {{.*}}, %[[ARG_HSE_PTR:.*]]: !cir.ptr<!rec_HasSideEffects>{{.*}})
     // CHECK-NEXT: %[[ARG_INT_ALLOCA:.*]] = cir.alloca "ArgInt" {{.*}} : !cir.ptr<!s32i>
     // CHECK-NEXT: %[[ARG_HSE_PTR_ALLOCA:.*]] = cir.alloca "ArgHSEPtr" {{.*}} : !cir.ptr<!cir.ptr<!rec_HasSideEffects>>
     // CHECK-NEXT: %[[LOC_HSE_ALLOCA:.*]] = cir.alloca "LocalHSE" {{.*}} : !cir.ptr<!rec_HasSideEffects>
diff --git a/clang/test/CIR/Transforms/abi-lowering/byval-sret-arg-attr-lowering.cir b/clang/test/CIR/Transforms/abi-lowering/byval-sret-arg-attr-lowering.cir
index c280a85119998..b0df1caa5416b 100644
--- a/clang/test/CIR/Transforms/abi-lowering/byval-sret-arg-attr-lowering.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/byval-sret-arg-attr-lowering.cir
@@ -18,7 +18,7 @@
   args   = [ ]
 }
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -56,13 +56,13 @@ module attributes {
   // CHECK: cir.func{{.*}} @ret_big(%{{.*}}: !cir.ptr<!rec_Big> {{{.*}}llvm.sret = !rec_Big{{.*}}})
   // LLVM: define void @ret_big(ptr dead_on_unwind noalias writable sret(%struct.Big) align 8 %{{.+}})
 
-  // byref carries the type the same way and is converted identically.
-  cir.func @takes_byref(%arg0: !rec_Big) attributes { test_classify = #byref_arg } {
+  // The non-byval form has no typed attribute, so there is nothing to remap.
+  cir.func @takes_non_byval(%arg0: !rec_Big) attributes { test_classify = #non_byval_arg } {
     cir.return
   }
 
-  // CHECK: cir.func{{.*}} @takes_byref(%{{.*}}: !cir.ptr<!rec_Big> {{{.*}}llvm.byref = !rec_Big{{.*}}})
-  // LLVM: define void @takes_byref(ptr byref(%struct.Big) align 8 %{{.+}})
+  // CHECK: cir.func{{.*}} @takes_non_byval(%{{.*}}: !cir.ptr<!rec_Big> {{{.*}}llvm.dereferenceable = 24 : i64, llvm.nofreeobj, llvm.noundef{{.*}}})
+  // LLVM: define void @takes_non_byval(ptr nofreeobj noundef align 8 dereferenceable(24) %{{.+}})
 
   // The call site's byval operand attribute is converted too.
   cir.func @caller(%s: !rec_Big) attributes { test_classify = #passthrough } {
diff --git a/clang/test/CIR/Transforms/abi-lowering/indirect-byval.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-byval.cir
index 9af556bad2661..90322dc342568 100644
--- a/clang/test/CIR/Transforms/abi-lowering/indirect-byval.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-byval.cir
@@ -33,36 +33,36 @@
   args   = [ { kind = "indirect", indirect_align = 8 } ]
 }
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
 
-#sret_byref = {
+#sret_non_byval = {
   return = { kind = "indirect", indirect_align = 8 },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
 
-#mixed_byref = {
+#mixed_non_byval = {
   return = { kind = "direct" },
   args   = [ { kind = "direct" },
              { kind = "indirect", indirect_align = 8, byval = false },
              { kind = "direct" } ]
 }
 
-#two_byref_args = {
+#two_non_byval_args = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false },
              { kind = "indirect", indirect_align = 8, byval = false } ]
 }
 
-#byref_then_direct = {
+#non_byval_then_direct = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false },
              { kind = "direct" } ]
 }
 
-#byref_arg_align16 = {
+#non_byval_arg_align16 = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 16, byval = false } ]
 }
@@ -197,30 +197,26 @@ module attributes {
   // CHECK:        cir.store %{{.*}}, %{{.*}} : !rec_Big, !cir.ptr<!rec_Big>
   // CHECK:        cir.return
 
-  // byref callee: llvm.byref without noundef.  CIRGen spills the
-  // by-value param into a local alloca; the rewriter rewires that alloca to
-  // the incoming pointer (no entry load / byte-copy).
-  cir.func @takes_big_byref(%arg0: !rec_Big) -> !rec_Big
-      attributes { test_classify = #byref_arg } {
+  // Non-byval callee.  CIRGen spills the by-value param into a local alloca.
+  // The rewriter rewires that alloca to the incoming pointer, with no entry
+  // load or byte-copy.
+  cir.func @takes_big_non_byval(%arg0: !rec_Big) -> !rec_Big
+      attributes { test_classify = #non_byval_arg } {
     %0 = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %0 : !rec_Big, !cir.ptr<!rec_Big>
     %1 = cir.load %0 : !cir.ptr<!rec_Big>, !rec_Big
     cir.return %1 : !rec_Big
   }
 
-  // CHECK:      cir.func{{.*}} @takes_big_byref(%[[PTR:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.align = 8 : i64
-  // CHECK-SAME:     llvm.byref = !rec_Big
-  // CHECK-NOT:    llvm.noalias
-  // CHECK-NOT:    llvm.noundef
+  // CHECK:      cir.func{{.*}} @takes_big_non_byval(%[[PTR:.*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK:        %[[L:.*]] = cir.load %[[PTR]] : !cir.ptr<!rec_Big>, !rec_Big
   // CHECK-NEXT:   cir.return %[[L]] : !rec_Big
 
-  // byref field access must use the incoming pointer in place — no local
-  // alloca and no byte-copy of the non-trivially-copyable aggregate.
-  cir.func @takes_big_byref_field(%arg0: !rec_Big) -> !s64i
-      attributes { test_classify = #byref_arg } {
+  // Field access must read through the incoming pointer, not a copy.
+  cir.func @takes_big_non_byval_field(%arg0: !rec_Big) -> !s64i
+      attributes { test_classify = #non_byval_arg } {
     %0 = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %0 : !rec_Big, !cir.ptr<!rec_Big>
     %1 = cir.get_member %0[0] {name = "a"} : !cir.ptr<!rec_Big> -> !cir.ptr<!s64i>
@@ -228,8 +224,8 @@ module attributes {
     cir.return %2 : !s64i
   }
 
-  // CHECK:      cir.func{{.*}} @takes_big_byref_field(%[[PTR:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:      cir.func{{.*}} @takes_big_non_byval_field(%[[PTR:.*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK:        %[[M:.*]] = cir.get_member %[[PTR]][0] {name = "a"} : !cir.ptr<!rec_Big> -> !cir.ptr<!s64i>
   // CHECK-NEXT:   %[[V:.*]] = cir.load %[[M]] : !cir.ptr<!s64i>, !s64i
@@ -255,152 +251,143 @@ module attributes {
   // CHECK:        %[[V:.*]] = cir.load %[[M]] : !cir.ptr<!s64i>, !s64i
   // CHECK:        cir.return %[[V]] : !s64i
 
-  // byref with the param spill removed (e.g. by DCE): the block arg has no
-  // use, so the rewriter only retypes it to a pointer and emits nothing else.
-  cir.func @takes_big_byref_unused(%arg0: !rec_Big) -> !s32i
-      attributes { test_classify = #byref_arg } {
+  // The param spill may already be gone, for instance to DCE.
+  cir.func @takes_big_non_byval_unused(%arg0: !rec_Big) -> !s32i
+      attributes { test_classify = #non_byval_arg } {
     %r = cir.const #cir.int<0> : !s32i
     cir.return %r : !s32i
   }
 
-  // CHECK:      cir.func{{.*}} @takes_big_byref_unused(%{{.*}}: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:      cir.func{{.*}} @takes_big_non_byval_unused(%{{.*}}: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK-NOT:    cir.load
   // CHECK:        %[[R:.*]] = cir.const #cir.int<0> : !s32i
   // CHECK-NEXT:   cir.return %[[R]] : !s32i
 
-  // byref forward declaration: signature gets llvm.byref, no body.
-  cir.func private @takes_big_byref_decl(%arg0: !rec_Big) -> !rec_Big
-      attributes { test_classify = #byref_arg }
+  // A declaration gets the attributes too, with no body to rewrite.
+  cir.func private @takes_big_non_byval_decl(%arg0: !rec_Big) -> !rec_Big
+      attributes { test_classify = #non_byval_arg }
 
-  // CHECK:      cir.func{{.*}} @takes_big_byref_decl(!cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
-  // CHECK-NOT:    llvm.noalias
-  // CHECK-NOT:    llvm.noundef
+  // CHECK:      cir.func{{.*}} @takes_big_non_byval_decl(!cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
 
-  // byref call site: the caller's temporary is passed rather than a copy, and
-  // the load that fed the call is left dead and erased.
-  cir.func @caller_byref(%s: !rec_Big) -> !rec_Big
+  // The call site forwards the caller's temporary instead of copying it.
+  cir.func @caller_non_byval(%s: !rec_Big) -> !rec_Big
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    %r = cir.call @takes_big_byref(%v) : (!rec_Big) -> !rec_Big
+    %r = cir.call @takes_big_non_byval(%v) : (!rec_Big) -> !rec_Big
     cir.return %r : !rec_Big
   }
 
-  // CHECK:      cir.func{{.*}} @caller_byref(%[[S:.*]]: !rec_Big) -> !rec_Big
+  // CHECK:      cir.func{{.*}} @caller_non_byval(%[[S:.*]]: !rec_Big) -> !rec_Big
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
   // CHECK-NEXT:   cir.store %[[S]], %[[TMP]] : !rec_Big, !cir.ptr<!rec_Big>
   // CHECK-NOT:    cir.load
-  // CHECK:        %{{.*}} = cir.call @takes_big_byref(%[[TMP]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
-  // CHECK-NOT:    llvm.noalias
-  // CHECK-NOT:    llvm.noundef
+  // CHECK:        %{{.*}} = cir.call @takes_big_non_byval(%[[TMP]]) :
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
 
   // The whole-record load survives when a reader other than the call needs it.
-  cir.func @caller_byref_load_reused(%s: !rec_Big) -> !rec_Big
+  cir.func @caller_non_byval_load_reused(%s: !rec_Big) -> !rec_Big
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    %r = cir.call @takes_big_byref(%v) : (!rec_Big) -> !rec_Big
+    %r = cir.call @takes_big_non_byval(%v) : (!rec_Big) -> !rec_Big
     cir.return %v : !rec_Big
   }
 
-  // CHECK:      cir.func{{.*}} @caller_byref_load_reused
+  // CHECK:      cir.func{{.*}} @caller_non_byval_load_reused
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
   // CHECK:        %[[V:.*]] = cir.load %[[TMP]] : !cir.ptr<!rec_Big>, !rec_Big
-  // CHECK:        %{{.*}} = cir.call @takes_big_byref(%[[TMP]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        %{{.*}} = cir.call @takes_big_non_byval(%[[TMP]]) :
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK:        cir.return %[[V]] : !rec_Big
 
   // CIRGen emits the load immediately before the call, so nothing can write
   // the temporary in between.  With a store in between the callee sees the
-  // stored value, since byref passes the object and not the loaded snapshot.
-  cir.func @caller_byref_store_between(%s: !rec_Big, %t: !rec_Big) -> !rec_Big
+  // stored value, since a non-byval argument passes the object rather than
+  // the loaded snapshot.
+  cir.func @caller_non_byval_store_between(%s: !rec_Big, %t: !rec_Big) -> !rec_Big
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
     cir.store %t, %tmp : !rec_Big, !cir.ptr<!rec_Big>
-    %r = cir.call @takes_big_byref(%v) : (!rec_Big) -> !rec_Big
+    %r = cir.call @takes_big_non_byval(%v) : (!rec_Big) -> !rec_Big
     cir.return %r : !rec_Big
   }
 
-  // CHECK:      cir.func{{.*}} @caller_byref_store_between(%[[S:.*]]: !rec_Big, %[[T:.*]]: !rec_Big)
+  // CHECK:      cir.func{{.*}} @caller_non_byval_store_between(%[[S:.*]]: !rec_Big, %[[T:.*]]: !rec_Big)
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
   // CHECK-NEXT:   cir.store %[[S]], %[[TMP]] : !rec_Big, !cir.ptr<!rec_Big>
   // CHECK-NEXT:   cir.store %[[T]], %[[TMP]] : !rec_Big, !cir.ptr<!rec_Big>
   // CHECK-NOT:    cir.load
-  // CHECK:        %{{.*}} = cir.call @takes_big_byref(%[[TMP]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        %{{.*}} = cir.call @takes_big_non_byval(%[[TMP]]) :
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
 
-  // A byref argument between two Direct ones keeps its operand slot, so the
-  // forwarded temporary lands at index 1 and its neighbours are untouched.
-  cir.func private @takes_mixed_byref(%a: !s32i, %b: !rec_Big, %c: !s32i)
-      attributes { test_classify = #mixed_byref }
+  // A non-byval argument between two Direct ones keeps its operand slot.
+  cir.func private @takes_mixed_non_byval(%a: !s32i, %b: !rec_Big, %c: !s32i)
+      attributes { test_classify = #mixed_non_byval }
 
-  cir.func @caller_mixed_byref(%x: !s32i, %s: !rec_Big)
+  cir.func @caller_mixed_non_byval(%x: !s32i, %s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_mixed_byref(%x, %v, %x) : (!s32i, !rec_Big, !s32i) -> ()
+    cir.call @takes_mixed_non_byval(%x, %v, %x) : (!s32i, !rec_Big, !s32i) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @caller_mixed_byref(%[[X:.*]]: !s32i
+  // CHECK:      cir.func{{.*}} @caller_mixed_non_byval(%[[X:.*]]: !s32i
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
-  // CHECK:        cir.call @takes_mixed_byref(%[[X]], %[[TMP]], %[[X]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        cir.call @takes_mixed_non_byval(%[[X]], %[[TMP]], %[[X]]) :
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef},
 
-  // One load feeding two byref operands: the alloca is forwarded to both and
-  // the shared load is erased once.
-  cir.func private @takes_two_byref(%a: !rec_Big, %b: !rec_Big)
-      attributes { test_classify = #two_byref_args }
+  // One load feeding two non-byval operands is erased once, not twice.
+  cir.func private @takes_two_non_byval(%a: !rec_Big, %b: !rec_Big)
+      attributes { test_classify = #two_non_byval_args }
 
-  cir.func @caller_two_byref_one_load(%s: !rec_Big)
+  cir.func @caller_two_non_byval_one_load(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_two_byref(%v, %v) : (!rec_Big, !rec_Big) -> ()
+    cir.call @takes_two_non_byval(%v, %v) : (!rec_Big, !rec_Big) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @caller_two_byref_one_load
+  // CHECK:      cir.func{{.*}} @caller_two_non_byval_one_load
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
   // CHECK-NEXT:   cir.store %{{.*}}, %[[TMP]] : !rec_Big, !cir.ptr<!rec_Big>
   // CHECK-NOT:    cir.load
-  // CHECK:        cir.call @takes_two_byref(%[[TMP]], %[[TMP]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        cir.call @takes_two_non_byval(%[[TMP]], %[[TMP]]) :
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}, !cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
 
   // The Direct operand keeps reading the load, so the load survives the rewrite.
-  cir.func private @takes_byref_then_direct(%a: !rec_Big, %b: !rec_Big)
-      attributes { test_classify = #byref_then_direct }
+  cir.func private @takes_non_byval_then_direct(%a: !rec_Big, %b: !rec_Big)
+      attributes { test_classify = #non_byval_then_direct }
 
-  cir.func @caller_byref_then_direct(%s: !rec_Big)
+  cir.func @caller_non_byval_then_direct(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_byref_then_direct(%v, %v) : (!rec_Big, !rec_Big) -> ()
+    cir.call @takes_non_byval_then_direct(%v, %v) : (!rec_Big, !rec_Big) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @caller_byref_then_direct
+  // CHECK:      cir.func{{.*}} @caller_non_byval_then_direct
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
   // CHECK:        %[[V:.*]] = cir.load %[[TMP]] : !cir.ptr<!rec_Big>, !rec_Big
-  // CHECK:        cir.call @takes_byref_then_direct(%[[TMP]], %[[V]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
-
-  // sret return plus a byref argument: the sret slot is prepended at operand 0,
-  // the forwarded temporary follows it, and the dead load is erased on the
-  // early-return sret path too.
-  cir.func @byref_and_sret(%arg0: !rec_Big) -> !rec_Big
-      attributes { test_classify = #sret_byref } {
+  // CHECK:        cir.call @takes_non_byval_then_direct(%[[TMP]], %[[V]]) :
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef},
+
+  // The dead load is erased on the sret early-return path too.
+  cir.func @non_byval_and_sret(%arg0: !rec_Big) -> !rec_Big
+      attributes { test_classify = #sret_non_byval } {
     %0 = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %0 : !rec_Big, !cir.ptr<!rec_Big>
     %1 = cir.alloca "__retval" align(8) : !cir.ptr<!rec_Big>
@@ -410,45 +397,42 @@ module attributes {
     cir.return %2 : !rec_Big
   }
 
-  cir.func @caller_byref_and_sret(%s: !rec_Big) -> !rec_Big
+  cir.func @caller_non_byval_and_sret(%s: !rec_Big) -> !rec_Big
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    %r = cir.call @byref_and_sret(%v) : (!rec_Big) -> !rec_Big
+    %r = cir.call @non_byval_and_sret(%v) : (!rec_Big) -> !rec_Big
     cir.return %r : !rec_Big
   }
 
-  // CHECK:      cir.func{{.*}} @caller_byref_and_sret
+  // CHECK:      cir.func{{.*}} @caller_non_byval_and_sret
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
   // CHECK-NEXT:   cir.store %{{.*}}, %[[TMP]] : !rec_Big, !cir.ptr<!rec_Big>
   // CHECK-NOT:    cir.load
   // CHECK:        %[[RET:.*]] = cir.alloca "sret" align(8) : !cir.ptr<!rec_Big>
-  // CHECK:        cir.call @byref_and_sret(%[[RET]], %[[TMP]]) :
+  // CHECK:        cir.call @non_byval_and_sret(%[[RET]], %[[TMP]]) :
   // CHECK-SAME:     llvm.sret = !rec_Big
-  // CHECK-SAME:     llvm.byref = !rec_Big
-  // CHECK-NOT:    llvm.noalias
-  // CHECK-NOT:    llvm.noundef
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
 
   // A slot aligned above the record's own alignment, matching an
   // indirect_align that also exceeds it, is still forwarded.
-  cir.func private @takes_big_byref16(%a: !rec_Big)
-      attributes { test_classify = #byref_arg_align16 }
+  cir.func private @takes_big_non_byval16(%a: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align16 }
 
-  cir.func @caller_byref_overaligned(%s: !rec_Big)
+  cir.func @caller_non_byval_overaligned(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(16) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref16(%v) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval16(%v) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @caller_byref_overaligned
+  // CHECK:      cir.func{{.*}} @caller_non_byval_overaligned
   // CHECK:        %[[TMP:.*]] = cir.alloca "agg.tmp" align(16) : !cir.ptr<!rec_Big>
   // CHECK-NOT:    cir.load
-  // CHECK:        cir.call @takes_big_byref16(%[[TMP]]) :
-  // CHECK-SAME:     llvm.align = 16 : i64
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        cir.call @takes_big_non_byval16(%[[TMP]]) :
+  // CHECK-SAME:     {llvm.align = 16 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
 
 }
diff --git a/clang/test/CIR/Transforms/abi-lowering/indirect-byref-forward-param.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir
similarity index 51%
rename from clang/test/CIR/Transforms/abi-lowering/indirect-byref-forward-param.cir
rename to clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir
index 05952a1083105..d7beeba906a4d 100644
--- a/clang/test/CIR/Transforms/abi-lowering/indirect-byref-forward-param.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir
@@ -5,7 +5,7 @@
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -15,29 +15,28 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  // A byref parameter handed on to another byref parameter, as a delegating or
+  // A non-byval parameter handed on to another one, as a delegating or
   // inherited constructor does.  Declaring the callee after the caller is what
   // makes the operand load the incoming pointer rather than a slot, so the
-  // order here is load bearing.
-  cir.func @forwards_byref_param(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg } {
+  // order here is load-bearing.
+  cir.func @forwards_non_byval_param(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg } {
     %spill = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%v) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @forwards_byref_param(%[[PTR:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.align = 8 : i64
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:      cir.func{{.*}} @forwards_non_byval_param(%[[PTR:.*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK-NOT:    cir.load
-  // CHECK:        cir.call @takes_big_byref(%[[PTR]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
 }
 
@@ -46,7 +45,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -59,24 +58,24 @@ module attributes {
   // The reverse order, where the operand still loads the parameter's own slot
   // and that slot is forwarded, then retargeted to the incoming pointer.  The
   // callee receives the caller's storage either way.
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
-  cir.func @forwards_byref_param_via_slot(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg } {
+  cir.func @forwards_non_byval_param_via_slot(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg } {
     %spill = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%v) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @forwards_byref_param_via_slot(%[[PTR:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:      cir.func{{.*}} @forwards_non_byval_param_via_slot(%[[PTR:.*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK-NOT:    cir.load
-  // CHECK:        cir.call @takes_big_byref(%[[PTR]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
 }
 
@@ -85,7 +84,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -97,7 +96,7 @@ module attributes {
 
   // Mutual recursion puts each function on both sides of the other, so both
   // orders above occur in one module.
-  cir.func @ping(%arg0: !rec_Big) attributes { test_classify = #byref_arg } {
+  cir.func @ping(%arg0: !rec_Big) attributes { test_classify = #non_byval_arg } {
     %spill = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
@@ -105,7 +104,7 @@ module attributes {
     cir.return
   }
 
-  cir.func @pong(%arg0: !rec_Big) attributes { test_classify = #byref_arg } {
+  cir.func @pong(%arg0: !rec_Big) attributes { test_classify = #non_byval_arg } {
     %spill = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
@@ -114,17 +113,17 @@ module attributes {
   }
 
   // CHECK:      cir.func{{.*}} @ping(%[[PING:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK-NOT:    cir.load
   // CHECK:        cir.call @pong(%[[PING]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
   // CHECK:      cir.func{{.*}} @pong(%[[PONG:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
   // CHECK-NOT:    cir.alloca
   // CHECK-NOT:    cir.load
   // CHECK:        cir.call @ping(%[[PONG]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
 }
 
@@ -134,12 +133,12 @@ module attributes {
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 !rec_Two = !cir.struct<"Two" {data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
 
-#expand_then_byref = {
+#expand_then_non_byval = {
   return = { kind = "direct" },
   args   = [ { kind = "expand" },
              { kind = "indirect", indirect_align = 8, byval = false } ]
@@ -150,25 +149,25 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  // An Expand parameter ahead of the byref one turns into two arguments, so
-  // the byref parameter sits at a different index than it was declared at.
-  // The attributes have to follow it there for the forward to be recognised.
-  cir.func @expand_ahead_of_byref(%two: !rec_Two, %big: !rec_Big)
-      attributes { test_classify = #expand_then_byref } {
+  // An Expand parameter ahead of the non-byval one turns into two arguments,
+  // so that parameter sits at a different index than it was declared at.
+  // The attributes have to follow it there for the forward to be recognized.
+  cir.func @expand_ahead_of_non_byval(%two: !rec_Two, %big: !rec_Big)
+      attributes { test_classify = #expand_then_non_byval } {
     %spill = cir.alloca "big" align(8) init : !cir.ptr<!rec_Big>
     cir.store %big, %spill : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%v) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK:      cir.func{{.*}} @expand_ahead_of_byref(%{{.*}}: !s64i, %{{.*}}: !s64i, %[[PTR:.*]]: !cir.ptr<!rec_Big>
-  // CHECK-SAME:     llvm.byref = !rec_Big
-  // CHECK:        cir.call @takes_big_byref(%[[PTR]]) :
-  // CHECK-SAME:     llvm.byref = !rec_Big
+  // CHECK:      cir.func{{.*}} @expand_ahead_of_non_byval(%{{.*}}: !s64i, %{{.*}}: !s64i, %[[PTR:.*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
+  // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
 }
 
@@ -178,7 +177,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -193,8 +192,8 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // A slot reached through casts that preserve the storage.  The slot states
   // the alignment, so it is found underneath the chain, while the outermost
@@ -207,7 +206,7 @@ module attributes {
     %slot = cir.cast bitcast %mid : !cir.ptr<!s8i> -> !cir.ptr<!rec_Big>
     cir.store %s, %slot : !rec_Big, !cir.ptr<!rec_Big>
     %v = cir.load %slot : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%v) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
     cir.return
   }
 
@@ -216,6 +215,53 @@ module attributes {
   // CHECK:        %[[MID:.*]] = cir.cast bitcast %[[RAW]] : !cir.ptr<!cir.array<!s8i x 32>> -> !cir.ptr<!s8i>
   // CHECK:        %[[SLOT:.*]] = cir.cast bitcast %[[MID]] : !cir.ptr<!s8i> -> !cir.ptr<!rec_Big>
   // CHECK-NOT:    cir.load %[[SLOT]] : !cir.ptr<!rec_Big>
-  // CHECK:        cir.call @takes_big_byref(%[[SLOT]]) : (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.byref = !rec_Big}) -> ()
+  // CHECK:        cir.call @takes_big_non_byval(%[[SLOT]]) : (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
+
+}
+
+// -----
+
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
+
+#non_byval_arg = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+#sret_non_byval = {
+  return = { kind = "indirect", indirect_align = 8 },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // The sret pointer takes block argument 0, so the forwarded parameter sits
+  // one past where it was declared.
+  cir.func @sret_forwards_non_byval_param(%arg0: !rec_Big) -> !rec_Big
+      attributes { test_classify = #sret_non_byval } {
+    %spill = cir.alloca "arg0" align(8) : !cir.ptr<!rec_Big>
+    cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
+    %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
+    %ret = cir.alloca "__retval" align(8) : !cir.ptr<!rec_Big>
+    %z = cir.const #cir.zero : !rec_Big
+    cir.store %z, %ret : !rec_Big, !cir.ptr<!rec_Big>
+    %r = cir.load %ret : !cir.ptr<!rec_Big>, !rec_Big
+    cir.return %r : !rec_Big
+  }
+
+  // CHECK:      cir.func{{.*}} @sret_forwards_non_byval_param(
+  // CHECK-SAME:     llvm.sret = !rec_Big
+  // CHECK-SAME:     %[[PTR:[^:]*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
+  // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
+
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
 }
diff --git a/clang/test/CIR/Transforms/abi-lowering/indirect-byref-nyi.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir
similarity index 56%
rename from clang/test/CIR/Transforms/abi-lowering/indirect-byref-nyi.cir
rename to clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir
index 4273692870fd8..8bdc6546c450e 100644
--- a/clang/test/CIR/Transforms/abi-lowering/indirect-byref-nyi.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir
@@ -5,7 +5,7 @@
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -20,19 +20,20 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // A record value with no storage behind it: nothing to forward.
-  cir.func @caller_byref_value(%novalue: !rec_Big)
+  cir.func @caller_non_byval_value(%novalue: !rec_Big)
       attributes { test_classify = #passthrough } {
-    cir.call @takes_big_byref(%novalue) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%novalue) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%novalue)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%novalue)
 
 }
 
@@ -41,7 +42,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -56,22 +57,23 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // A volatile load is an observable access the look-through would delete.
-  cir.func @caller_byref_volatile(%s: !rec_Big)
+  cir.func @caller_non_byval_volatile(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %volatileload = cir.load volatile %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%volatileload) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%volatileload) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%volatileload)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%volatileload)
 
 }
 
@@ -80,7 +82,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -95,23 +97,24 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // An atomic load carries ordering the look-through would drop.
-  cir.func @caller_byref_atomic(%s: !rec_Big)
+  cir.func @caller_non_byval_atomic(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %atomicload = cir.load align(8) atomic(seq_cst) %tmp
         : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%atomicload) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%atomicload) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%atomicload)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%atomicload)
 
 }
 
@@ -121,7 +124,7 @@ module attributes {
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 !rec_Outer = !cir.struct<"Outer" {data !rec_Big}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -136,25 +139,26 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // A load of a member of an enclosing record is not the temporary the caller
   // destroys, so it is reported rather than forwarded.
-  cir.func @caller_byref_member(%s: !rec_Outer)
+  cir.func @caller_non_byval_member(%s: !rec_Outer)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "outer" align(8) : !cir.ptr<!rec_Outer>
     cir.store %s, %tmp : !rec_Outer, !cir.ptr<!rec_Outer>
     %member = cir.get_member %tmp[0] {name = "inner"}
         : !cir.ptr<!rec_Outer> -> !cir.ptr<!rec_Big>
     %memberload = cir.load %member : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%memberload) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%memberload) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%memberload)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%memberload)
 
 }
 
@@ -163,7 +167,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg_align16 = {
+#non_byval_arg_align16 = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 16, byval = false } ]
 }
@@ -178,25 +182,26 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref16(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg_align16 }
+  cir.func private @takes_big_non_byval16(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align16 }
 
   // A slot stating less alignment than the callee is told the argument has.
   // Raising it would not survive a slot that stands in for a parameter, so it
   // is reported rather than forwarded under an alignment the callee may rely
   // on.
-  cir.func @caller_byref_underaligned(%s: !rec_Big)
+  cir.func @caller_non_byval_underaligned(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %tmp = cir.alloca "agg.tmp" align(8) : !cir.ptr<!rec_Big>
     cir.store %s, %tmp : !rec_Big, !cir.ptr<!rec_Big>
     %underaligned = cir.load %tmp : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref16(%underaligned) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval16(%underaligned) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref16(%underaligned)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval16(%underaligned)
 
 }
 
@@ -205,7 +210,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -223,23 +228,24 @@ module attributes {
   // A return slot holds the object the caller reads after the call, not one it
   // destroys, so it is not forwardable even though it becomes a pointer
   // parameter carrying llvm.align.
-  cir.func @caller_byref_from_sret_slot() -> !rec_Big
+  cir.func @caller_non_byval_from_sret_slot() -> !rec_Big
       attributes { test_classify = #sret_return } {
     %ret = cir.alloca "__retval" align(8) : !cir.ptr<!rec_Big>
     %z = cir.const #cir.zero : !rec_Big
     cir.store %z, %ret : !rec_Big, !cir.ptr<!rec_Big>
     %retslot = cir.load %ret : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%retslot) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%retslot) : (!rec_Big) -> ()
     %r = cir.load %ret : !cir.ptr<!rec_Big>, !rec_Big
     cir.return %r : !rec_Big
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%retslot)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%retslot)
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
 }
 
@@ -248,7 +254,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -264,21 +270,22 @@ module attributes {
 } {
 
   // A byval parameter is the callee's own copy, which nothing else destroys,
-  // so handing it on as byref would give the inner callee an object with the
-  // wrong owner.  Its pointer carries llvm.align too, so llvm.byref is what
-  // separates the two.
-  cir.func @caller_byref_from_byval_param(%byvalparam: !rec_Big)
+  // so handing it on would give the inner callee an object with the wrong
+  // owner.  Only a non-byval parameter is recorded as forwardable, so this
+  // one is rejected.
+  cir.func @caller_non_byval_from_byval_param(%byvalparam: !rec_Big)
       attributes { test_classify = #byval_arg } {
-    cir.call @takes_big_byref(%byvalparam) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%byvalparam) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%byvalparam)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%byvalparam)
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
 }
 
@@ -287,12 +294,12 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg_align8 = {
+#non_byval_arg_align8 = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
 
-#byref_arg_align16 = {
+#non_byval_arg_align16 = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 16, byval = false } ]
 }
@@ -302,23 +309,24 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  // A byref parameter states less alignment than the callee it is handed to
+  // A non-byval parameter states less alignment than the callee it is handed to
   // is told about, so it cannot stand in for that argument.
-  cir.func @caller_byref_param_underaligned(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg_align8 } {
+  cir.func @caller_non_byval_param_underaligned(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align8 } {
     %spill = cir.alloca "arg0" align(16) init : !cir.ptr<!rec_Big>
     cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
     %paramload = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref16(%paramload) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval16(%paramload) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref16(%paramload)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval16(%paramload)
 
-  cir.func private @takes_big_byref16(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg_align16 }
+  cir.func private @takes_big_non_byval16(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align16 }
 
 }
 
@@ -327,7 +335,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -342,25 +350,26 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // A decay names one element of an array, not the whole allocation, so
   // getUnderlyingAlloca does not look through it.
-  cir.func @caller_byref_through_decay(%s: !rec_Big)
+  cir.func @caller_non_byval_through_decay(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %arr = cir.alloca "arr" align(8) : !cir.ptr<!cir.array<!rec_Big x 2>>
     %decayed = cir.cast array_to_ptrdecay %arr
         : !cir.ptr<!cir.array<!rec_Big x 2>> -> !cir.ptr<!rec_Big>
     cir.store %s, %decayed : !rec_Big, !cir.ptr<!rec_Big>
     %decayload = cir.load %decayed : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref(%decayload) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%decayload) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%decayload)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%decayload)
 
 }
 
@@ -370,7 +379,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg_align16 = {
+#non_byval_arg_align16 = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 16, byval = false } ]
 }
@@ -385,25 +394,26 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref16(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg_align16 }
+  cir.func private @takes_big_non_byval16(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align16 }
 
   // The alignment comes from the slot underneath the cast, not from the cast,
   // so an under-aligned slot is still reported once a cast is in between.
-  cir.func @caller_byref_underaligned_through_cast(%s: !rec_Big)
+  cir.func @caller_non_byval_underaligned_through_cast(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %raw = cir.alloca "raw" align(8) : !cir.ptr<!cir.array<!s8i x 32>>
     %slot = cir.cast bitcast %raw
         : !cir.ptr<!cir.array<!s8i x 32>> -> !cir.ptr<!rec_Big>
     cir.store %s, %slot : !rec_Big, !cir.ptr<!rec_Big>
     %castunderaligned = cir.load %slot : !cir.ptr<!rec_Big>, !rec_Big
-    cir.call @takes_big_byref16(%castunderaligned) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval16(%castunderaligned) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref16(%castunderaligned)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval16(%castunderaligned)
 
 }
 
@@ -412,7 +422,7 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
-#byref_arg = {
+#non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
 }
@@ -427,13 +437,13 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  cir.func private @takes_big_byref(%arg0: !rec_Big)
-      attributes { test_classify = #byref_arg }
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
 
   // The slot underneath the cast is forwardable, but the rewritten parameter
   // is a pointer in the default address space, so the operand cannot be
   // handed on as it stands.
-  cir.func @caller_byref_through_addrspace(%s: !rec_Big)
+  cir.func @caller_non_byval_through_addrspace(%s: !rec_Big)
       attributes { test_classify = #passthrough } {
     %raw = cir.alloca "raw" align(8) : !cir.ptr<!rec_Big>
     %slot = cir.cast address_space %raw
@@ -441,12 +451,54 @@ module attributes {
     cir.store %s, %slot : !rec_Big, !cir.ptr<!rec_Big, target_address_space(5)>
     %asload = cir.load %slot
         : !cir.ptr<!rec_Big, target_address_space(5)>, !rec_Big
-    cir.call @takes_big_byref(%asload) : (!rec_Big) -> ()
+    cir.call @takes_big_non_byval(%asload) : (!rec_Big) -> ()
     cir.return
   }
 
-  // CHECK: error: 'cir.call' op byref argument that does not name the
-  // CHECK-SAME: caller's storage is not yet implemented in CallConvLowering
-  // CHECK-NEXT: cir.call @takes_big_byref(%asload)
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%asload)
+
+}
+
+// -----
+
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
+
+#non_byval_arg = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+#direct_ptr = {
+  return = { kind = "direct" },
+  args   = [ { kind = "direct" } ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
+
+  // A Direct pointer parameter that carries the same attributes the pass puts
+  // on a non-byval one.  It is not one, so it must not be forwarded.
+  cir.func @caller_lookalike_attrs(%lookalike: !cir.ptr<!rec_Big> {
+        llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64,
+        llvm.nofreeobj, llvm.noundef})
+      attributes { test_classify = #direct_ptr } {
+    %v = cir.load %lookalike : !cir.ptr<!rec_Big>, !rec_Big
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
+    cir.return
+  }
+
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval(%v)
 
 }
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir
index d7f7d2566adb3..9f443a7e70e7e 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir
@@ -199,8 +199,8 @@ module attributes {
     cir.return
   }
 
-  // CHECK: cir.func{{.*}} @take_ntc_empty(%arg0: !cir.ptr<!rec_NTCE> {llvm.align = 1 : i64, llvm.byref = !rec_NTCE})
-  // LLVM: define void @take_ntc_empty(ptr byref(%struct.NTCE) align 1 %{{.*}})
+  // CHECK: cir.func{{.*}} @take_ntc_empty(%arg0: !cir.ptr<!rec_NTCE> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nofreeobj, llvm.noundef})
+  // LLVM: define void @take_ntc_empty(ptr nofreeobj noundef align 1 dereferenceable(1) %{{.*}})
 
   // The same record returned goes through sret rather than being dropped.
   cir.func @ret_ntc_empty() -> !rec_NTCE {
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-record-packed.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-record-packed.cir
index e0b162b781ba4..90eca9f09b3e5 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-record-packed.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-record-packed.cir
@@ -72,14 +72,14 @@ module attributes {
   // CHECK:   cir.store %arg1, %[[HI]] : !s8i, !cir.ptr<!s8i>
   // CHECK:   %{{.+}} = cir.cast bitcast %[[RECSLOT]] : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!rec_Nine>
 
-  // A packed record the caller must destroy takes byref rather than byval, and
-  // byref keeps the record's declared alignment where byval would raise it to
-  // the ABI minimum of 8.
+  // A packed record the caller must destroy takes the non-byval form rather
+  // than byval, which keeps the record's declared alignment where byval would
+  // raise it to the ABI minimum of 8.
   cir.func @take_p(%arg0: !rec_P) {
     cir.return
   }
 
-  // CHECK-LABEL: cir.func{{.*}} @take_p(%arg0: !cir.ptr<!rec_P> {llvm.align = 1 : i64, llvm.byref = !rec_P})
+  // CHECK-LABEL: cir.func{{.*}} @take_p(%arg0: !cir.ptr<!rec_P> {llvm.align = 1 : i64, llvm.dereferenceable = 5 : i64, llvm.nofreeobj, llvm.noundef})
 
   // A union coerces through the same one-register path as FiveShort.
   cir.func @take_upacked(%arg0: !rec_UPacked) {
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
index bc75ab74b445f..c843ff0026604 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
@@ -52,7 +52,7 @@ module attributes {
     cir.return
   }
 
-  // CHECK: cir.func{{.*}} @take_noregs(%arg0: !cir.ptr<!rec_NoRegs> {llvm.align = 4 : i64, llvm.byref = !rec_NoRegs})
+  // CHECK: cir.func{{.*}} @take_noregs(%arg0: !cir.ptr<!rec_NoRegs> {llvm.align = 4 : i64, llvm.dereferenceable = 8 : i64, llvm.nofreeobj, llvm.noundef})
 
   // {__int128, char} spans more than two eightbytes, so it does not fit in
   // registers: the argument is byval and the return uses sret.
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-padded.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-padded.cir
index 8bdd1d4640e63..de30f61c0a329 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-padded.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-padded.cir
@@ -81,12 +81,13 @@ module attributes {
   // CHECK: cir.func{{.*}} @take_big(%arg0: !cir.ptr<!rec_Big> {llvm.align = 16 : i64, llvm.byval = !rec_Big, llvm.noundef})
 
   // A record the caller must destroy is passed by reference rather than copied,
-  // so it takes byref instead of byval even though the size rule is the same.
+  // so it takes the non-byval form instead of byval even though the size rule
+  // is the same.
   cir.func @take_no_regs(%arg0: !rec_NoRegs) {
     cir.return
   }
 
-  // CHECK: cir.func{{.*}} @take_no_regs(%arg0: !cir.ptr<!rec_NoRegs> {llvm.align = 16 : i64, llvm.byref = !rec_NoRegs})
+  // CHECK: cir.func{{.*}} @take_no_regs(%arg0: !cir.ptr<!rec_NoRegs> {llvm.align = 16 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef})
 
   // An anonymous record has no entry in cir.record_layouts, so both the
   // register-passing decision and the alignment come from the fallbacks.
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir
index dd3066848c460..e41f9b451f379 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir
@@ -392,7 +392,7 @@ module attributes {
     cir.return
   }
 
-  // CHECK: cir.func{{.*}} @take_no_regs(%arg0: !cir.ptr<!rec_UNoRegs> {llvm.align = 4 : i64, llvm.byref = !rec_UNoRegs})
+  // CHECK: cir.func{{.*}} @take_no_regs(%arg0: !cir.ptr<!rec_UNoRegs> {llvm.align = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nofreeobj, llvm.noundef})
 
   // A struct member that is itself a union is mapped through the same union
   // handling, so the enclosing 8-byte struct coerces to one i64.
@@ -496,7 +496,7 @@ module attributes {
 // LLVM: define void @ret_empty_only()
 // LLVM: define void @call_empty_int(i32 %{{.+}})
 // LLVM:   call void @take_empty_int(i32 %{{.+}})
-// LLVM: define void @take_no_regs(ptr byref(%union.UNoRegs) align 4 %{{.+}})
+// LLVM: define void @take_no_regs(ptr nofreeobj noundef align 4 dereferenceable(4) %{{.+}})
 // LLVM: define void @take_struct_with_union(i64 %{{.+}})
 // LLVM: define i32 @ret_int_float(i32 %{{.+}})
 // LLVM: define void @ret_big(ptr dead_on_unwind noalias writable sret(%union.UBig) align 1 %{{.+}}, ptr noundef byval(%union.UBig) align 8 %{{.+}})
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir
index de10afb52eed8..0a662367695e5 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-vptr.cir
@@ -183,7 +183,7 @@ module attributes {
 
   // Past two eightbytes SysV says memory whatever the eightbytes hold.  A
   // record eligible for registers reaches memory by the byval path, not the
-  // byref path a cannot-pass-in-registers record takes.
+  // non-byval path a cannot-pass-in-registers record takes.
   cir.func @take_vp_big(%arg0: !rec_VPBig) {
     cir.return
   }
@@ -208,10 +208,10 @@ module attributes {
     cir.return
   }
 
-  // CHECK: cir.func{{.*}} @take_vp_no_regs(%arg0: !cir.ptr<!rec_VPNoRegs> {llvm.align = 8 : i64, llvm.byref = !rec_VPNoRegs})
-  // LLVM: define void @take_vp_no_regs(ptr byref(%struct.VPNoRegs) align 8 %{{[^,)]+}})
+  // CHECK: cir.func{{.*}} @take_vp_no_regs(%arg0: !cir.ptr<!rec_VPNoRegs> {llvm.align = 8 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef})
+  // LLVM: define void @take_vp_no_regs(ptr nofreeobj noundef align 8 dereferenceable(16) %{{[^,)]+}})
 
-  // The same record returned takes sret rather than byref.
+  // The same record returned takes sret rather than the non-byval form.
   cir.func @ret_vp_no_regs() -> !rec_VPNoRegs {
     %0 = cir.alloca "r" align(8) : !cir.ptr<!rec_VPNoRegs>
     %1 = cir.load %0 : !cir.ptr<!rec_VPNoRegs>, !rec_VPNoRegs

>From b9279bcc1013fc08bfa7ceaa8194bf573ae8cca3 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 10 Sep 2026 12:13:38 -0700
Subject: [PATCH 2/3] [CIR] Mark the argument attributes CIR does not emit yet

The two missing features that were documented in comments become
MissingFeatures markers.

Assisted-by: Cursor / claude-opus-5
---
 clang/include/clang/CIR/MissingFeatures.h     |  2 ++
 .../TargetLowering/CIRABIRewriteContext.cpp   | 19 +++++++++++--------
 2 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index 23e903f197cfa..16d97871ec7b6 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -219,6 +219,8 @@ struct MissingFeatures {
   static bool hipModuleCtor() { return false; }
   static bool dataLayoutTypeAllocSize() { return false; }
   static bool dataLayoutPtrHandlingBasedOnLangAS() { return false; }
+  static bool deadOnReturnAttr() { return false; }
+  static bool noaliasOnByvalAttr() { return false; }
   static bool deferredCXXGlobalInit() { return false; }
   static bool deleteArray() { return false; }
   static bool devirtualizeDestructor() { return false; }
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index a8070decedcf8..7091b76d8f668 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -13,6 +13,7 @@
 #include "clang/CIR/Dialect/IR/CIRAttrs.h"
 #include "clang/CIR/Dialect/IR/CIRDialect.h"
 #include "clang/CIR/Dialect/IR/CIRTypes.h"
+#include "clang/CIR/MissingFeatures.h"
 #include <algorithm>
 
 using namespace cir;
@@ -219,14 +220,9 @@ mlir::ArrayAttr updateArgAttrs(mlir::MLIRContext *ctx,
       // which constrains the pointer operand, not the pointee's contents.
       //
       // llvm.byval(T) records the pre-rewrite arg type because the opaque
-      // LLVM pointer cannot carry it.  llvm.nofreeobj holds because a
-      // parameter has automatic storage duration.
-      //
-      // Two of classic's attributes are missing.  llvm.noalias needs
-      // -fpass-by-value-is-noalias, which CIR does not plumb through.
-      // llvm.dead_on_return needs the destructor's triviality, which
-      // cir.record_layout carries as has_trivial_dtor and nothing here reads
-      // yet.
+      // LLVM pointer cannot carry it.  llvm.nofreeobj says the object cannot
+      // be freed while the callee runs, which holds because the caller owns it
+      // across the call.
       mlir::Type pointeeTy = origArgTypes[oldIdx];
       mlir::NamedAttrList attrs(existing);
       attrs.set(mlir::LLVM::LLVMDialect::getAlignAttrName(),
@@ -234,9 +230,16 @@ mlir::ArrayAttr updateArgAttrs(mlir::MLIRContext *ctx,
       attrs.set(mlir::LLVM::LLVMDialect::getNoUndefAttrName(),
                 builder.getUnitAttr());
       if (ac.byVal) {
+        // Classic adds llvm.noalias under -fpass-by-value-is-noalias, which
+        // CIR does not plumb through.
+        assert(!cir::MissingFeatures::noaliasOnByvalAttr());
         attrs.set(mlir::LLVM::LLVMDialect::getByValAttrName(),
                   mlir::TypeAttr::get(pointeeTy));
       } else {
+        // Classic adds llvm.dead_on_return when the object's lifetime ends in
+        // the callee, which needs the destructor's triviality from
+        // cir.record_layout's has_trivial_dtor.
+        assert(!cir::MissingFeatures::deadOnReturnAttr());
         attrs.set(mlir::LLVM::LLVMDialect::getNoFreeObjAttrName(),
                   builder.getUnitAttr());
         attrs.set(mlir::LLVM::LLVMDialect::getDereferenceableAttrName(),

>From 21379e5fa681317b29cf0e6ed76e38d54054dd96 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 10 Sep 2026 12:36:20 -0700
Subject: [PATCH 3/3] [CIR] Defer collapsing a non-byval parameter's slot

The map recording non-byval parameters existed only because the pass
erased the slot a later call site needed to recognise them by.  Keeping
the slot until every call site has been rewritten removes the need to
record anything, and drops one arm of the forwarding predicate.

A slot also states the alignment CIRGen chose for a local copy rather
than the one the ABI promises, and a call is rewritten with its callee
rather than its containing function, so reading it made forwarding
depend on declaration order.  Restate each slot up front instead.

Assisted-by: Cursor / claude-opus-5
---
 .../Transforms/CallConvLoweringPass.cpp       |  16 ++
 .../TargetLowering/CIRABIRewriteContext.cpp   | 136 ++++++++++-------
 .../TargetLowering/CIRABIRewriteContext.h     |  49 ++++--
 .../indirect-non-byval-forward-param.cir      | 140 +++++++++++++++++-
 .../abi-lowering/indirect-non-byval-nyi.cir   |  43 ++++++
 5 files changed, 312 insertions(+), 72 deletions(-)

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 7af53c0698885..3ed7f2671a7ea 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -48,6 +48,7 @@
 #include "llvm/ABI/FunctionInfo.h"
 #include "llvm/ABI/TargetInfo.h"
 #include "llvm/ABI/Types.h"
+#include "llvm/ADT/ScopeExit.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/TypeSwitch.h"
 #include "llvm/IR/CallingConv.h"
@@ -864,6 +865,12 @@ void CallConvLoweringPass::runOnOperation() {
 
   DataLayout dl(moduleOp);
   CIRABIRewriteContext rewriteCtx(moduleOp, dl);
+  // A non-byval indirect parameter's slot outlives the rewrite that retypes
+  // the parameter, so that a call forwarding the parameter can still recognise
+  // it.  Draining on scope exit collapses those slots whichever way this
+  // function returns.
+  llvm::scope_exit drainParamSlots(
+      [&] { rewriteCtx.finalizeParameterSlots(); });
   SymbolTable symbolTable(moduleOp);
 
   // A per-function target attribute can raise the AVX level, so one classifier
@@ -1002,6 +1009,15 @@ void CallConvLoweringPass::runOnOperation() {
     addressTakers[callee].push_back(getGlobal);
   });
 
+  // Restate every non-byval indirect parameter's slot alignment as the one the
+  // ABI promises for that parameter, before anything reads a slot.  A call is
+  // rewritten together with its callee rather than with the function
+  // containing it, so a call forwarding such a parameter can be reached before
+  // the parameter's own function is rewritten.  Doing this up front makes the
+  // forwarding decision independent of the order the two were declared in.
+  for (auto &kv : classifications)
+    rewriteCtx.normalizeParameterSlotAlignments(kv.first, kv.second);
+
   // Rewrite each function together with every direct call to it and every op
   // holding its address.  By the time we move on to function F+1, F's
   // signature and every reference to F have already been brought into
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index 7091b76d8f668..7ac358835be58 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -14,7 +14,9 @@
 #include "clang/CIR/Dialect/IR/CIRDialect.h"
 #include "clang/CIR/Dialect/IR/CIRTypes.h"
 #include "clang/CIR/MissingFeatures.h"
+#include "llvm/ADT/STLExtras.h"
 #include <algorithm>
+#include <utility>
 
 using namespace cir;
 using namespace mlir;
@@ -454,26 +456,18 @@ static cir::LoadOp getWholeRecordLoad(mlir::Value recordVal) {
 
 /// Whether a non-byval indirect argument may name \p addr, given the callee is
 /// told the argument is \p minAlign aligned.  A slot allocated here qualifies,
-/// reached through storage-preserving casts, and so does a non-byval indirect
-/// parameter of the enclosing function, matched by exact value.  Which arm
-/// answers depends on whether the driver reached this call before the
-/// definition enclosing it.  For CIRGen output they agree, since the spill
-/// slot states the alignment the classification does.
+/// reached through storage-preserving casts, and so does the enclosing
+/// function's own non-byval parameter: its slot stands until
+/// finalizeParameterSlots, and states the alignment the parameter promises
+/// rather than the one CIRGen chose for a local copy.
 ///
-/// Both must already state that alignment: a slot's own alignment can be
-/// raised in principle, but one standing in for a parameter is replaced by the
-/// incoming pointer later, which would discard the raise and leave the callee
-/// over-promised.
-static bool forwardableNonByvalStorage(
-    mlir::Value addr, uint64_t minAlign,
-    const llvm::DenseMap<mlir::BlockArgument, uint64_t> &nonByvalParams) {
-  if (cir::AllocaOp slot = cir::getUnderlyingAlloca(addr))
-    return slot.getAlignment() >= minAlign;
-  auto blockArg = mlir::dyn_cast<mlir::BlockArgument>(addr);
-  if (!blockArg || !blockArg.getOwner()->isEntryBlock())
-    return false;
-  auto param = nonByvalParams.find(blockArg);
-  return param != nonByvalParams.end() && param->second >= minAlign;
+/// The slot must already state that alignment.  Raising it here would not
+/// survive one that stands in for a parameter, since finalizeParameterSlots
+/// replaces it with the incoming pointer, which would discard the raise and
+/// leave the callee over-promised.
+static bool forwardableNonByvalStorage(mlir::Value addr, uint64_t minAlign) {
+  cir::AllocaOp slot = cir::getUnderlyingAlloca(addr);
+  return slot && slot.getAlignment() >= minAlign;
 }
 
 /// Decompose a struct value into one scalar call argument per field of \p
@@ -525,13 +519,31 @@ static void eraseDeadRecordLoads(ArrayRef<cir::LoadOp> loads) {
       load->erase();
 }
 
+/// The store that spills non-byval indirect parameter \p blockArg, and the
+/// slot it spills into.  CIRGen spills every by-value parameter into a local
+/// alloca with a single store before any other use, and this pass runs on that
+/// CIRGen output before any alloca-promoting or splitting pass, so the block
+/// argument has exactly that one use.  Both results are null when DCE already
+/// removed a dead spill.
+static std::pair<cir::StoreOp, cir::AllocaOp>
+findParamSpill(mlir::BlockArgument blockArg) {
+  if (blockArg.use_empty())
+    return {};
+  assert(blockArg.hasOneUse() &&
+         "non-byval arg must have exactly one use (the CIRGen param spill)");
+  auto store = cast<cir::StoreOp>(*blockArg.user_begin());
+  assert(store.getValue() == blockArg &&
+         "non-byval arg's use must be the value operand of its store");
+  return {store, cast<cir::AllocaOp>(store.getAddr().getDefiningOp())};
+}
+
 /// For each Direct arg with a coerced type, change the block argument's type
 /// to the coerced type and insert a coercion at function entry that maps it
 /// back to the original type for body uses.  For each Indirect byval arg,
 /// change the block argument's type to a pointer and insert a load at entry
 /// so the body sees a local copy of the original value type.  For each
-/// Indirect non-byval arg, change the block argument to a pointer and
-/// rewire the CIRGen param-slot alloca to that pointer (no entry load /
+/// Indirect non-byval arg, change the block argument to a pointer and queue
+/// the CIRGen param-slot alloca to be replaced by it (no entry load /
 /// byte-copy) so the body operates on the caller's storage in place.  For each
 /// Expand arg, replace the single struct block argument with N scalar block
 /// arguments (one per field) and store each field directly into the parameter's
@@ -545,7 +557,8 @@ static void eraseDeadRecordLoads(ArrayRef<cir::LoadOp> loads) {
 void insertArgCoercion(
     mlir::FunctionOpInterface funcOp, const FunctionClassification &fc,
     mlir::OpBuilder &builder, const mlir::DataLayout &dl, bool hasSRetArg,
-    llvm::DenseMap<mlir::BlockArgument, uint64_t> &nonByvalParams) {
+    SmallVectorImpl<std::pair<cir::AllocaOp, mlir::BlockArgument>>
+        &pendingParamSlots) {
   mlir::Region &body = funcOp->getRegion(0);
   if (body.empty())
     return;
@@ -719,28 +732,12 @@ void insertArgCoercion(
       auto ptrTy = cir::PointerType::get(blockArg.getType());
 
       if (!ac.byVal) {
-        // Without byval: CIRGen spills every by-value parameter into a local
-        // alloca with a single store before any other use, and this pass runs
-        // on that CIRGen output before any alloca-promoting or splitting pass,
-        // so the block argument still has exactly that one use here.
-        // Rewire the alloca to the incoming pointer and drop the store so the
-        // body operates on the caller's storage in place.  A byte-copy would be
-        // wrong for non-trivially-copyable aggregates (e.g. libstdc++ SSO
-        // std::string, where it would leave `_M_p` aliasing the source's
-        // `_M_local_buf`).  DCE may have removed a dead spill; tolerate that by
-        // only retyping the block argument.
-        cir::StoreOp paramStore;
-        cir::AllocaOp destAlloca;
-        if (!blockArg.use_empty()) {
-          assert(blockArg.hasOneUse() &&
-                 "non-byval arg must have exactly one use (the CIRGen param "
-                 "spill)");
-          paramStore = cast<cir::StoreOp>(*blockArg.user_begin());
-          assert(paramStore.getValue() == blockArg &&
-                 "non-byval arg's use must be the value operand of its store");
-          destAlloca =
-              cast<cir::AllocaOp>(paramStore.getAddr().getDefiningOp());
-        }
+        // Without byval, drop the spill store and let the slot's uses read the
+        // incoming pointer, so the body operates on the caller's storage in
+        // place.  A byte-copy would be wrong for non-trivially-copyable
+        // aggregates (e.g. libstdc++ SSO std::string, where it would leave
+        // `_M_p` aliasing the source's `_M_local_buf`).
+        auto [paramStore, destAlloca] = findParamSpill(blockArg);
 
         if (paramStore)
           paramStore->erase();
@@ -748,12 +745,14 @@ void insertArgCoercion(
         // Update the block argument to point to its original type.
         blockArg.setType(ptrTy);
 
-        nonByvalParams[blockArg] = ac.indirectAlign.value();
-
-        if (destAlloca) {
-          destAlloca.getResult().replaceAllUsesWith(blockArg);
-          destAlloca->erase();
-        }
+        // Pointing the slot's uses at the incoming pointer waits until every
+        // call site has been rewritten.  A call that hands this parameter
+        // straight on recognises it by the slot its operand was loaded from,
+        // and collapsing the slot here would leave that call reading a block
+        // argument with no defining operation to inspect.  A dead spill DCE
+        // already removed leaves nothing to collapse.
+        if (destAlloca)
+          pendingParamSlots.emplace_back(destAlloca, blockArg);
       } else {
         // byval: load the incoming pointer so the body sees a T value (and
         // any CIRGen param-slot store becomes a local copy of that value).
@@ -1020,6 +1019,37 @@ void rewriteIndirectReturnCall(cir::CallOp call,
 
 } // namespace
 
+void CIRABIRewriteContext::normalizeParameterSlotAlignments(
+    cir::FuncOp funcOp, const FunctionClassification &fc) {
+  if (!funcOp.isDefinition())
+    return;
+  mlir::Region &body = funcOp->getRegion(0);
+  if (body.empty())
+    return;
+  mlir::Block &entry = body.front();
+
+  // No signature has been rewritten yet, so no sret pointer has been prepended
+  // and no Expand argument has been split into its fields.  Every
+  // classification therefore still maps to the entry block argument at its own
+  // index.
+  for (auto [idx, ac] : llvm::enumerate(fc.argInfos)) {
+    if (ac.kind != ArgKind::Indirect || ac.byVal)
+      continue;
+    assert(idx < entry.getNumArguments() &&
+           "classification count must not exceed entry block arguments");
+    if (cir::AllocaOp slot = findParamSpill(entry.getArgument(idx)).second)
+      slot.setAlignment(ac.indirectAlign.value());
+  }
+}
+
+void CIRABIRewriteContext::finalizeParameterSlots() {
+  for (auto [slot, incoming] : pendingParamSlots) {
+    slot.getResult().replaceAllUsesWith(incoming);
+    slot->erase();
+  }
+  pendingParamSlots.clear();
+}
+
 mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
     mlir::FunctionOpInterface funcOpInterface, const FunctionClassification &fc,
     mlir::OpBuilder &builder) {
@@ -1086,7 +1116,7 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
       // in-body cir.call operands) through the recovered value.  Done before
       // the Ignore-drop below so the entry block argument indices used here
       // still refer to the original positions.
-      insertArgCoercion(funcOp, fc, builder, dl, hasSRet, nonByvalParams);
+      insertArgCoercion(funcOp, fc, builder, dl, hasSRet, pendingParamSlots);
 
       // Direct return with coerced type: insert a coercion at every
       // cir.return so the returned value matches the (coerced) return
@@ -1300,8 +1330,8 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
         if (!srcLoad ||
             srcLoad.getAddr().getType() !=
                 cir::PointerType::get(arg.getType()) ||
-            !forwardableNonByvalStorage(
-                srcLoad.getAddr(), ac.indirectAlign.value(), nonByvalParams))
+            !forwardableNonByvalStorage(srcLoad.getAddr(),
+                                        ac.indirectAlign.value()))
           return call->emitOpError()
                  << "non-byval indirect argument that does not name the "
                     "caller's storage is not yet implemented in "
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h
index 7d977cf026c5b..b23359f8e5dee 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.h
@@ -24,7 +24,9 @@
 #include "mlir/IR/BuiltinOps.h"
 #include "mlir/Interfaces/DataLayoutInterfaces.h"
 #include "clang/CIR/Dialect/IR/CIRDialect.h"
-#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/SmallVector.h"
+#include <cassert>
+#include <utility>
 
 namespace cir {
 
@@ -41,6 +43,11 @@ class CIRABIRewriteContext : public mlir::abi::ABIRewriteContext {
   CIRABIRewriteContext(mlir::ModuleOp module, const mlir::DataLayout &dl)
       : module(module), dl(dl) {}
 
+  ~CIRABIRewriteContext() {
+    assert(pendingParamSlots.empty() &&
+           "finalizeParameterSlots must run before the rewrite context dies");
+  }
+
   mlir::LogicalResult
   rewriteFunctionDefinition(mlir::FunctionOpInterface funcOp,
                             const mlir::abi::FunctionClassification &fc,
@@ -59,24 +66,40 @@ class CIRABIRewriteContext : public mlir::abi::ABIRewriteContext {
   void rewriteFunctionAddress(cir::GetGlobalOp addrOp, cir::FuncOp funcOp,
                               mlir::OpBuilder &builder);
 
+  /// Restate each non-byval indirect parameter's CIRGen slot alignment as the
+  /// alignment the ABI promises for that parameter.  CIRGen picked the slot's
+  /// alignment for a local copy of the record, but the slot is about to stand
+  /// in for the parameter, and a call forwarding it may only promise what the
+  /// incoming pointer does.  Call for every function before any call site is
+  /// rewritten, since a call is rewritten with its callee rather than with its
+  /// enclosing function and so may be reached first.  Not an override, since
+  /// this has no counterpart in the generic contract.
+  void
+  normalizeParameterSlotAlignments(cir::FuncOp funcOp,
+                                   const mlir::abi::FunctionClassification &fc);
+
+  /// Replace each non-byval indirect parameter's CIRGen slot with the
+  /// incoming pointer, so the body operates on the caller's storage in place.
+  /// Call once, after every function and call site has been rewritten: a call
+  /// forwarding such a parameter reads the slot to recognise it.  Not an
+  /// override, since deferring this has no counterpart in the generic
+  /// contract.
+  void finalizeParameterSlots();
+
   mlir::StringRef getDialectNamespace() const override { return "cir"; }
 
 private:
   mlir::ModuleOp module;
   const mlir::DataLayout &dl;
 
-  /// Each block argument rewriteFunctionDefinition has rewritten into a
-  /// non-byval indirect parameter, mapped to the alignment its classification
-  /// states, so rewriteCallSite can forward such a parameter rather than copy
-  /// it.  Recorded where the classification says so rather than read back
-  /// from an emitted attribute, which would tie the pass to whichever
-  /// attribute is unique to this case today.
-  ///
-  /// Sound for one run over one module only.  A recorded argument is retyped
-  /// but never erased, so the keys stay valid, but a value freed with one
-  /// module can be recycled by the next, and a stale hit would forward the
-  /// caller's object where a copy is required.  Do not promote to pass state.
-  llvm::DenseMap<mlir::BlockArgument, uint64_t> nonByvalParams;
+  /// CIRGen param-slot allocas that non-byval indirect parameters will
+  /// replace, paired with the incoming pointer that replaces them.  The
+  /// rewrite retypes the block argument but leaves the slot standing, because
+  /// a call site recognises a forwardable parameter by the slot its operand
+  /// was loaded from.  finalizeParameterSlots does the replacement once every
+  /// call site has been rewritten.
+  llvm::SmallVector<std::pair<cir::AllocaOp, mlir::BlockArgument>>
+      pendingParamSlots;
 };
 
 } // namespace cir
diff --git a/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir
index d7beeba906a4d..2fd34fd3aaedd 100644
--- a/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-forward-param.cir
@@ -16,9 +16,10 @@ module attributes {
 } {
 
   // A non-byval parameter handed on to another one, as a delegating or
-  // inherited constructor does.  Declaring the callee after the caller is what
-  // makes the operand load the incoming pointer rather than a slot, so the
-  // order here is load-bearing.
+  // inherited constructor does.  A call is rewritten together with its callee,
+  // so declaring the callee second means the caller's own definition has
+  // already been rewritten, and its slot already queued for collapsing, by the
+  // time the call is reached.
   cir.func @forwards_non_byval_param(%arg0: !rec_Big)
       attributes { test_classify = #non_byval_arg } {
     %spill = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Big>
@@ -55,9 +56,9 @@ module attributes {
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
 } {
 
-  // The reverse order, where the operand still loads the parameter's own slot
-  // and that slot is forwarded, then retargeted to the incoming pointer.  The
-  // callee receives the caller's storage either way.
+  // The reverse declaration order, so the call is reached before the caller's
+  // own definition is rewritten and the slot is still untouched.  The callee
+  // receives the caller's storage either way.
   cir.func private @takes_big_non_byval(%arg0: !rec_Big)
       attributes { test_classify = #non_byval_arg }
 
@@ -163,6 +164,7 @@ module attributes {
 
   // CHECK:      cir.func{{.*}} @expand_ahead_of_non_byval(%{{.*}}: !s64i, %{{.*}}: !s64i, %[[PTR:.*]]: !cir.ptr<!rec_Big>
   // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
+  // CHECK-NOT:    cir.alloca
   // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
   // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
@@ -258,6 +260,7 @@ module attributes {
   // CHECK-SAME:     llvm.sret = !rec_Big
   // CHECK-SAME:     %[[PTR:[^:]*]]: !cir.ptr<!rec_Big>
   // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
+  // CHECK-NOT:    cir.alloca "arg0"
   // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
   // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
 
@@ -265,3 +268,128 @@ module attributes {
       attributes { test_classify = #non_byval_arg }
 
 }
+
+// -----
+
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
+
+#non_byval_arg = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // The slot states the alignment CIRGen picked for a local copy, which can be
+  // less than the parameter promises.  The forward is judged against the
+  // parameter's, so it holds here regardless of which of the two was declared
+  // first.
+  cir.func private @takes_big_non_byval(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg }
+
+  cir.func @forwards_param_over_underaligned_slot(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg } {
+    %spill = cir.alloca "arg0" align(4) init : !cir.ptr<!rec_Big>
+    cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
+    %v = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
+    cir.call @takes_big_non_byval(%v) : (!rec_Big) -> ()
+    cir.return
+  }
+
+  // CHECK:      cir.func{{.*}} @forwards_param_over_underaligned_slot(%[[PTR:.*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef})
+  // CHECK-NOT:    cir.alloca
+  // CHECK:        cir.call @takes_big_non_byval(%[[PTR]]) :
+  // CHECK-SAME:     (!cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dereferenceable = 32 : i64, llvm.nofreeobj, llvm.noundef}) -> ()
+
+}
+
+// -----
+
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
+
+#two_non_byval = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false },
+             { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // Two parameters in one function, forwarded in the opposite order, so a slot
+  // paired with the wrong incoming pointer would swap the operands.
+  cir.func @forwards_two_non_byval_params(%x: !rec_Big, %y: !rec_Big)
+      attributes { test_classify = #two_non_byval } {
+    %sx = cir.alloca "x" align(8) init : !cir.ptr<!rec_Big>
+    %sy = cir.alloca "y" align(8) init : !cir.ptr<!rec_Big>
+    cir.store %x, %sx : !rec_Big, !cir.ptr<!rec_Big>
+    cir.store %y, %sy : !rec_Big, !cir.ptr<!rec_Big>
+    %vx = cir.load %sx : !cir.ptr<!rec_Big>, !rec_Big
+    %vy = cir.load %sy : !cir.ptr<!rec_Big>, !rec_Big
+    cir.call @sink_two(%vy, %vx) : (!rec_Big, !rec_Big) -> ()
+    cir.return
+  }
+
+  // CHECK:      cir.func{{.*}} @forwards_two_non_byval_params(%[[X:[^:]*]]: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     %[[Y:[^:]*]]: !cir.ptr<!rec_Big>
+  // CHECK-NOT:    cir.alloca
+  // CHECK:        cir.call @sink_two(%[[Y]], %[[X]]) :
+
+  cir.func private @sink_two(%a: !rec_Big, %b: !rec_Big)
+      attributes { test_classify = #two_non_byval }
+
+}
+
+// -----
+
+!s64i = !cir.int<s, 64>
+!rec_Two = !cir.struct<"Two" {data !s64i, data !s64i}>
+
+#non_byval_two = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+#expand_two = {
+  return = { kind = "direct" },
+  args   = [ { kind = "expand" } ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // A non-byval parameter feeding an Expand callee.  The fields are read from
+  // the caller's storage with cir.get_member rather than extracted from a
+  // whole-record value.
+  cir.func private @takes_two_expanded(%arg0: !rec_Two)
+      attributes { test_classify = #expand_two }
+
+  cir.func @expands_non_byval_param(%arg0: !rec_Two)
+      attributes { test_classify = #non_byval_two } {
+    %spill = cir.alloca "arg0" align(8) init : !cir.ptr<!rec_Two>
+    cir.store %arg0, %spill : !rec_Two, !cir.ptr<!rec_Two>
+    %v = cir.load %spill : !cir.ptr<!rec_Two>, !rec_Two
+    cir.call @takes_two_expanded(%v) : (!rec_Two) -> ()
+    cir.return
+  }
+
+  // CHECK:      cir.func{{.*}} @expands_non_byval_param(%[[PTR:.*]]: !cir.ptr<!rec_Two>
+  // CHECK-SAME:     {llvm.align = 8 : i64, llvm.dereferenceable = 16 : i64, llvm.nofreeobj, llvm.noundef})
+  // CHECK-NOT:    cir.alloca
+  // CHECK:        %[[F0:.*]] = cir.get_member %[[PTR]][0]
+  // CHECK:        %[[V0:.*]] = cir.load %[[F0]]
+  // CHECK:        %[[F1:.*]] = cir.get_member %[[PTR]][1]
+  // CHECK:        %[[V1:.*]] = cir.load %[[F1]]
+  // CHECK:        cir.call @takes_two_expanded(%[[V0]], %[[V1]]) :
+
+}
diff --git a/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir
index 8bdc6546c450e..864d88f490ee0 100644
--- a/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-non-byval-nyi.cir
@@ -335,6 +335,49 @@ module attributes {
 !s64i = !cir.int<s, 64>
 !rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
 
+#non_byval_arg_align8 = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+#non_byval_arg_align16 = {
+  return = { kind = "direct" },
+  args   = [ { kind = "indirect", indirect_align = 16, byval = false } ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // The same rejection with the callee declared first, so the call is reached
+  // before the caller's own definition is rewritten.  The slot's own alignment
+  // would satisfy the callee here, and only the parameter's promise rules the
+  // forward out.
+  cir.func private @takes_big_non_byval16(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align16 }
+
+  cir.func @callee_first_non_byval_param_underaligned(%arg0: !rec_Big)
+      attributes { test_classify = #non_byval_arg_align8 } {
+    %spill = cir.alloca "arg0" align(16) init : !cir.ptr<!rec_Big>
+    cir.store %arg0, %spill : !rec_Big, !cir.ptr<!rec_Big>
+    %paramload = cir.load %spill : !cir.ptr<!rec_Big>, !rec_Big
+    cir.call @takes_big_non_byval16(%paramload) : (!rec_Big) -> ()
+    cir.return
+  }
+
+  // CHECK: error: 'cir.call' op non-byval indirect argument that
+  // CHECK-SAME: does not name the caller's storage is not yet
+  // CHECK-SAME: implemented in CallConvLowering
+  // CHECK-NEXT: cir.call @takes_big_non_byval16(%paramload)
+
+}
+
+// -----
+
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
+
 #non_byval_arg = {
   return = { kind = "direct" },
   args   = [ { kind = "indirect", indirect_align = 8, byval = false } ]



More information about the cfe-commits mailing list