[clang] 43511b5 - [CIR] Add Extend (signext/zeroext) handling to CallConvLowering (#195745)
via cfe-commits
cfe-commits at lists.llvm.org
Fri May 29 11:46:59 PDT 2026
Author: adams381
Date: 2026-05-29T13:46:53-05:00
New Revision: 43511b564751fa0231d012b8bda59b88915d5855
URL: https://github.com/llvm/llvm-project/commit/43511b564751fa0231d012b8bda59b88915d5855
DIFF: https://github.com/llvm/llvm-project/commit/43511b564751fa0231d012b8bda59b88915d5855.diff
LOG: [CIR] Add Extend (signext/zeroext) handling to CallConvLowering (#195745)
Third PR in the series splitting
[#192119](https://github.com/llvm/llvm-project/pull/192119) /
[#192124](https://github.com/llvm/llvm-project/pull/192124).
[#195725](https://github.com/llvm/llvm-project/pull/195725) and
[#195737](https://github.com/llvm/llvm-project/pull/195737) have merged;
this PR is now a standalone diff on main.
Adds Extend (signext / zeroext) to `cir-call-conv-lowering`. The CIR
signature keeps the original narrow integer type; the rewriter attaches
`llvm.signext` / `llvm.zeroext` to `arg_attrs` and `res_attrs`. That
matches classic Clang's LLVM IR convention — `define void @f(i8 signext
%x)`, not `define void @f(i32 signext %x)` with an entry-block
truncation. The `coercedType` field on an Extend `ArgClassification` is
informational only; the rewriter doesn't use it to change the CIR
signature.
Three `.cir` tests cover narrow-signed-arg, narrow-unsigned-arg, and
narrow-signed-return. Since the test target's narrow-int Extend rule
fires only on MLIR `IntegerType` and CIR functions use `cir::IntType`,
these tests drive the rewriter through the classification-injection path
added in [#195725](https://github.com/llvm/llvm-project/pull/195725).
Added:
clang/test/CIR/Transforms/abi-lowering/extend-after-ignore.cir
clang/test/CIR/Transforms/abi-lowering/extend-return.cir
clang/test/CIR/Transforms/abi-lowering/extend-signed-arg.cir
clang/test/CIR/Transforms/abi-lowering/extend-unsigned-arg.cir
Modified:
clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
Removed:
################################################################################
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index d33346cc0dcb3..2c29c83b999ba 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -27,6 +27,10 @@ using namespace mlir::abi;
namespace {
bool needsRewrite(const FunctionClassification &fc) {
+ // Direct without coercion is a true pass-through; any other kind (or a
+ // coerced Direct) means the rewriter must touch the IR. Extend is
+ // technically attribute-only at the IR level but still counts because the
+ // attribute attachment changes observable behavior.
if ((fc.returnInfo.kind != ArgKind::Direct) || fc.returnInfo.coercedType)
return true;
for (const ArgClassification &ac : fc.argInfos)
@@ -35,14 +39,6 @@ bool needsRewrite(const FunctionClassification &fc) {
return false;
}
-SmallVector<unsigned> ignoredArgIndices(const FunctionClassification &fc) {
- SmallVector<unsigned> v;
- for (auto [idx, ac] : llvm::enumerate(fc.argInfos))
- if (ac.kind == ArgKind::Ignore)
- v.push_back(idx);
- return v;
-}
-
/// Build the new argument-type list for a function whose ABI classification
/// is \p fc. This currently handles only Direct (no coercion) and Ignore;
/// other kinds emit an error. Classifications that add arguments (e.g.
@@ -72,9 +68,14 @@ LogicalResult buildNewArgTypes(ArrayRef<Type> oldArgTypes,
<< " not yet implemented in CallConvLowering";
return failure();
case ArgKind::Extend:
- emitError() << "Extend at arg " << idx
- << " not yet implemented in CallConvLowering";
- return failure();
+ // Extend keeps the original (narrow) type in the signature; the
+ // sign/zero extension is communicated to LLVM via the llvm.signext /
+ // llvm.zeroext arg attribute, attached separately below. Any
+ // coercedType the classifier set on the Extend ArgClassification is
+ // informational (typically the register-width type the value gets
+ // extended to in registers) but does not change the CIR signature.
+ newArgTypes.push_back(origTy);
+ break;
case ArgKind::Indirect:
emitError() << "Indirect at arg " << idx
<< " not yet implemented in CallConvLowering";
@@ -105,8 +106,10 @@ Type computeNewReturnType(Type origRetTy, const ArgClassification &retInfo,
<< "it in EmitFunctionEpilog)";
return nullptr;
case ArgKind::Extend:
- emitError() << "Extend return not yet implemented in CallConvLowering";
- return nullptr;
+ // Same convention as Extend args: keep the original return type in the
+ // signature; the sign/zero extension is communicated via the
+ // llvm.signext / llvm.zeroext res attribute attached separately below.
+ return origRetTy;
case ArgKind::Indirect:
emitError() << "Indirect return (sret) not yet implemented in "
<< "CallConvLowering";
@@ -124,17 +127,65 @@ Value createIgnoredValue(OpBuilder &builder, Location loc, Type ty) {
return cir::ConstantOp::create(builder, loc, ty, cir::PoisonAttr::get(ty));
}
+/// Build an updated arg_attrs ArrayAttr that drops Ignore'd args and adds
+/// llvm.signext / llvm.zeroext on Extend args. Preserves any existing arg
+/// attributes on retained arg slots.
+ArrayAttr updateArgAttrs(MLIRContext *ctx, ArrayAttr existingArgAttrs,
+ const FunctionClassification &fc) {
+ SmallVector<Attribute> newArgAttrs;
+ newArgAttrs.reserve(fc.argInfos.size());
+ for (auto [oldIdx, ac] : llvm::enumerate(fc.argInfos)) {
+ if (ac.kind == ArgKind::Ignore)
+ continue;
+ DictionaryAttr existing = DictionaryAttr::get(ctx);
+ if (existingArgAttrs && oldIdx < existingArgAttrs.size())
+ existing = cast<DictionaryAttr>(existingArgAttrs[oldIdx]);
+ if (ac.kind == ArgKind::Extend) {
+ StringRef attrName = ac.signExtend ? "llvm.signext" : "llvm.zeroext";
+ NamedAttribute extAttr(StringAttr::get(ctx, attrName),
+ UnitAttr::get(ctx));
+ if (existing.empty()) {
+ newArgAttrs.push_back(DictionaryAttr::get(ctx, {extAttr}));
+ } else {
+ SmallVector<NamedAttribute> attrs(existing.begin(), existing.end());
+ attrs.push_back(extAttr);
+ newArgAttrs.push_back(DictionaryAttr::get(ctx, attrs));
+ }
+ } else {
+ newArgAttrs.push_back(existing);
+ }
+ }
+ return ArrayAttr::get(ctx, newArgAttrs);
+}
+
+/// Build an updated res_attrs ArrayAttr (single entry, since CIR funcs have
+/// at most one result) that adds llvm.signext / llvm.zeroext on an Extend
+/// return. Preserves any existing res attributes.
+ArrayAttr updateResAttrs(MLIRContext *ctx, ArrayAttr existingResAttrs,
+ const ArgClassification &retInfo) {
+ if (retInfo.kind != ArgKind::Extend)
+ return existingResAttrs;
+
+ SmallVector<NamedAttribute> attrs;
+ if (existingResAttrs && !existingResAttrs.empty())
+ for (NamedAttribute na : cast<DictionaryAttr>(existingResAttrs[0]))
+ attrs.push_back(na);
+ StringRef attrName = retInfo.signExtend ? "llvm.signext" : "llvm.zeroext";
+ attrs.push_back(
+ NamedAttribute(StringAttr::get(ctx, attrName), UnitAttr::get(ctx)));
+ return ArrayAttr::get(ctx, {DictionaryAttr::get(ctx, attrs)});
+}
+
} // namespace
LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
FunctionOpInterface funcOpInterface, const FunctionClassification &fc,
OpBuilder &builder) {
- // The pass driver (CallConvLoweringPass) only ever hands us cir.func ops,
- // and the body of this routine is end-to-end CIR (it creates cir.constant,
- // cir.return, etc.). Cast once at the top so the rest of the function
- // reads in CIR's own vocabulary, and so we can dispatch to the
- // CIRGlobalValueInterface for isDefinition() (FunctionOpInterface alone
- // does not inherit from CIRGlobalValueInterface).
+ // The pass driver (CallConvLoweringPass) only ever hands us cir.func ops.
+ // Cast once at the top so the rest of the function reads in CIR's own
+ // vocabulary, and so we can dispatch to the CIRGlobalValueInterface for
+ // isDefinition() (FunctionOpInterface alone does not inherit from
+ // CIRGlobalValueInterface).
cir::FuncOp funcOp = cast<cir::FuncOp>(funcOpInterface);
if (!needsRewrite(fc))
@@ -172,10 +223,13 @@ LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
// body still references it, replace those uses with a poison
// constant. Ignore classifications mean the value is empty / not
// passed at the ABI level, so any remaining uses are vacuous;
- // poison says exactly that.
- SmallVector<unsigned> ignored = ignoredArgIndices(fc);
- for (unsigned blockIdx : llvm::reverse(ignored)) {
- if (blockIdx >= entry.getNumArguments())
+ // poison says exactly that. Iterate in reverse so that earlier
+ // indices stay stable as later ones are erased.
+ for (int blockIdx = static_cast<int>(fc.argInfos.size()) - 1;
+ blockIdx >= 0; --blockIdx) {
+ if (fc.argInfos[blockIdx].kind != ArgKind::Ignore)
+ continue;
+ if (static_cast<unsigned>(blockIdx) >= entry.getNumArguments())
continue;
BlockArgument arg = entry.getArgument(blockIdx);
if (!arg.use_empty()) {
@@ -212,22 +266,22 @@ LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
Type newFnTy = funcOp.cloneTypeWith(newArgTypes, newResultTypes);
funcOp.setFunctionTypeAttr(TypeAttr::get(newFnTy));
- // Keep the arg_attrs array in sync with the new argument count by
- // dropping entries for every Ignored argument. Without this the
- // attribute array would have stale entries that no longer match any
- // block argument.
- SmallVector<unsigned> ignored = ignoredArgIndices(fc);
- if (!ignored.empty()) {
- if (auto existing = funcOp->getAttrOfType<ArrayAttr>("arg_attrs")) {
- SmallVector<Attribute> kept;
- kept.reserve(newArgTypes.size());
- for (auto [oldIdx, attr] : llvm::enumerate(existing.getValue())) {
- if (oldIdx >= fc.argInfos.size() ||
- fc.argInfos[oldIdx].kind != ArgKind::Ignore)
- kept.push_back(attr);
- }
- funcOp->setAttr("arg_attrs", ArrayAttr::get(ctx, kept));
- }
+ // Rebuild arg_attrs when any arg is Ignore (dropped from the output array)
+ // or Extend (needs llvm.signext / llvm.zeroext layered on).
+ bool needsArgAttrUpdate =
+ llvm::any_of(fc.argInfos, [](const ArgClassification &ac) {
+ return ac.kind == ArgKind::Ignore || ac.kind == ArgKind::Extend;
+ });
+ if (needsArgAttrUpdate) {
+ auto existing = funcOp->getAttrOfType<ArrayAttr>("arg_attrs");
+ funcOp->setAttr("arg_attrs", updateArgAttrs(ctx, existing, fc));
+ }
+
+ // Rebuild res_attrs: layer llvm.signext / llvm.zeroext onto an Extend
+ // return.
+ if (fc.returnInfo.kind == ArgKind::Extend) {
+ auto existing = funcOp->getAttrOfType<ArrayAttr>("res_attrs");
+ funcOp->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
}
return success();
@@ -247,6 +301,8 @@ LogicalResult CIRABIRewriteContext::rewriteCallSite(
return call.emitOpError()
<< "indirect call not yet implemented in CallConvLowering";
+ MLIRContext *ctx = callOp->getContext();
+
for (auto [idx, ac] : llvm::enumerate(fc.argInfos)) {
switch (ac.kind) {
case ArgKind::Direct:
@@ -261,8 +317,9 @@ LogicalResult CIRABIRewriteContext::rewriteCallSite(
return call.emitOpError() << "Expand at call-site arg " << idx
<< " not yet implemented in CallConvLowering";
case ArgKind::Extend:
- return call.emitOpError() << "Extend at call-site arg " << idx
- << " not yet implemented in CallConvLowering";
+ // Extend at the call site is just an attribute change (llvm.signext /
+ // llvm.zeroext on the call's arg_attrs); no IR-level cast.
+ break;
case ArgKind::Indirect:
return call.emitOpError() << "Indirect at call-site arg " << idx
<< " not yet implemented in CallConvLowering";
@@ -284,15 +341,13 @@ LogicalResult CIRABIRewriteContext::rewriteCallSite(
}
bool hasResult = call.getNumResults() > 0;
- Type origRetTy = hasResult ? call.getResult().getType()
- : cir::VoidType::get(callOp->getContext());
+ Type origRetTy =
+ hasResult ? call.getResult().getType() : cir::VoidType::get(ctx);
Type callRetTy = origRetTy;
if (fc.returnInfo.kind == ArgKind::Ignore && hasResult)
- callRetTy = cir::VoidType::get(callOp->getContext());
- if ((fc.returnInfo.kind == ArgKind::Direct ||
- fc.returnInfo.kind == ArgKind::Extend) &&
- fc.returnInfo.coercedType)
- return call.emitOpError() << "Direct/Extend return with coerced type at "
+ callRetTy = cir::VoidType::get(ctx);
+ if (fc.returnInfo.kind == ArgKind::Direct && fc.returnInfo.coercedType)
+ return call.emitOpError() << "Direct return with coerced type at "
<< "call-site not yet implemented in "
<< "CallConvLowering";
@@ -303,6 +358,22 @@ LogicalResult CIRABIRewriteContext::rewriteCallSite(
if (!newCall->hasAttr(attr.getName()))
newCall->setAttr(attr.getName(), attr.getValue());
+ // Layer llvm.signext / llvm.zeroext onto the new call's arg_attrs and
+ // res_attrs for Extend args/return. Ignore args also require a rebuild
+ // because their slots are dropped from the output array.
+ bool needsArgAttrUpdate =
+ llvm::any_of(fc.argInfos, [](const ArgClassification &ac) {
+ return ac.kind == ArgKind::Ignore || ac.kind == ArgKind::Extend;
+ });
+ if (needsArgAttrUpdate) {
+ auto existing = call->getAttrOfType<ArrayAttr>("arg_attrs");
+ newCall->setAttr("arg_attrs", updateArgAttrs(ctx, existing, fc));
+ }
+ if (fc.returnInfo.kind == ArgKind::Extend) {
+ auto existing = call->getAttrOfType<ArrayAttr>("res_attrs");
+ newCall->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
+ }
+
if (hasResult && fc.returnInfo.kind == ArgKind::Ignore) {
// The new call returns void, but the original call's result may still
// have uses. Substitute a poison constant of the original type so
diff --git a/clang/test/CIR/Transforms/abi-lowering/extend-after-ignore.cir b/clang/test/CIR/Transforms/abi-lowering/extend-after-ignore.cir
new file mode 100644
index 0000000000000..d19c511206b62
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/extend-after-ignore.cir
@@ -0,0 +1,44 @@
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: | FileCheck %s
+
+!s8i = !cir.int<s, 8>
+!s32i = !cir.int<s, 32>
+
+// Direct(0), Ignore(1), Extend-signed(2): the Ignore slot drops from the
+// signature and the Extend slot shifts to position 1. llvm.signext must
+// land on the shifted position, not the original one.
+#direct_ignore_extend = {
+ return = { kind = "direct" },
+ args = [ { kind = "direct" },
+ { kind = "ignore" },
+ { kind = "extend",
+ coerced_type = !s32i,
+ sign_extend = true } ]
+}
+
+module attributes {
+ dlti.dl_spec = #dlti.dl_spec<
+ #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+ #dlti.dl_entry<i32, dense<32>: vector<2xi64>>>
+} {
+
+ cir.func @callee(%arg0: !s32i, %arg1: !s32i, %arg2: !s8i) -> !s32i
+ attributes { test_classify = #direct_ignore_extend } {
+ cir.return %arg0 : !s32i
+ }
+
+ // Ignore slot drops; Extend shifts to position 1 and gets llvm.signext.
+ // CHECK: cir.func{{.*}} @callee(%arg0: !s32i, %arg1: !s8i {llvm.signext}) -> !s32i
+ // CHECK-NEXT: cir.return %arg0 : !s32i
+
+ cir.func @caller(%arg0: !s32i, %arg1: !s32i, %arg2: !s8i) -> !s32i
+ attributes { test_classify = #direct_ignore_extend } {
+ %0 = cir.call @callee(%arg0, %arg1, %arg2) : (!s32i, !s32i, !s8i) -> !s32i
+ cir.return %0 : !s32i
+ }
+
+ // CHECK: cir.func{{.*}} @caller(%arg0: !s32i, %arg1: !s8i {llvm.signext}) -> !s32i
+ // CHECK-NEXT: %[[R:.*]] = cir.call @callee(%arg0, %arg1) : (!s32i, !s8i {llvm.signext}) -> !s32i
+ // CHECK-NEXT: cir.return %[[R]] : !s32i
+
+}
diff --git a/clang/test/CIR/Transforms/abi-lowering/extend-return.cir b/clang/test/CIR/Transforms/abi-lowering/extend-return.cir
new file mode 100644
index 0000000000000..1d07ec1468809
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/extend-return.cir
@@ -0,0 +1,38 @@
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: | FileCheck %s
+
+!s8i = !cir.int<s, 8>
+
+#extend_signed_return = {
+ return = { kind = "extend",
+ coerced_type = !cir.int<s, 32>,
+ sign_extend = true },
+ args = [ ]
+}
+
+module attributes {
+ dlti.dl_spec = #dlti.dl_spec<
+ #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+ #dlti.dl_entry<i32, dense<32>: vector<2xi64>>>
+} {
+
+ cir.func @returns_s8() -> !s8i
+ attributes { test_classify = #extend_signed_return } {
+ %0 = cir.const #cir.int<7> : !s8i
+ cir.return %0 : !s8i
+ }
+
+ // CHECK: cir.func{{.*}} @returns_s8() -> (!s8i {llvm.signext})
+ // CHECK: cir.return %{{.*}} : !s8i
+
+ cir.func @caller() -> !s8i
+ attributes { test_classify = #extend_signed_return } {
+ %0 = cir.call @returns_s8() : () -> !s8i
+ cir.return %0 : !s8i
+ }
+
+ // CHECK: cir.func{{.*}} @caller() -> (!s8i {llvm.signext})
+ // CHECK: %[[R:.*]] = cir.call @returns_s8() : () -> (!s8i {llvm.signext})
+ // CHECK: cir.return %[[R]] : !s8i
+
+}
diff --git a/clang/test/CIR/Transforms/abi-lowering/extend-signed-arg.cir b/clang/test/CIR/Transforms/abi-lowering/extend-signed-arg.cir
new file mode 100644
index 0000000000000..499db952ccf42
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/extend-signed-arg.cir
@@ -0,0 +1,41 @@
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: | FileCheck %s
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: -cir-to-llvm -o - 2>/dev/null \
+// RUN: | mlir-translate -mlir-to-llvmir --allow-unregistered-dialect \
+// RUN: | FileCheck %s --check-prefix=LLVM
+
+!s8i = !cir.int<s, 8>
+
+#extend_signed_arg = {
+ return = { kind = "direct" },
+ args = [ { kind = "extend",
+ coerced_type = !cir.int<s, 32>,
+ sign_extend = true } ]
+}
+
+module attributes {
+ dlti.dl_spec = #dlti.dl_spec<
+ #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+ #dlti.dl_entry<i32, dense<32>: vector<2xi64>>>
+} {
+
+ cir.func @takes_s8(%arg0: !s8i)
+ attributes { test_classify = #extend_signed_arg } {
+ cir.return
+ }
+
+ // CHECK: cir.func{{.*}} @takes_s8(%arg0: !s8i {llvm.signext})
+
+ cir.func @caller(%arg0: !s8i)
+ attributes { test_classify = #extend_signed_arg } {
+ cir.call @takes_s8(%arg0) : (!s8i) -> ()
+ cir.return
+ }
+
+ // CHECK: cir.call @takes_s8(%arg0) : (!s8i {llvm.signext}) -> ()
+
+}
+
+// LLVM: define void @takes_s8(i8 signext %{{.+}})
+// LLVM: define void @caller(i8 signext %{{.+}})
diff --git a/clang/test/CIR/Transforms/abi-lowering/extend-unsigned-arg.cir b/clang/test/CIR/Transforms/abi-lowering/extend-unsigned-arg.cir
new file mode 100644
index 0000000000000..8c3e01c6328df
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/extend-unsigned-arg.cir
@@ -0,0 +1,34 @@
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: | FileCheck %s
+
+!u16i = !cir.int<u, 16>
+
+#extend_unsigned_arg = {
+ return = { kind = "direct" },
+ args = [ { kind = "extend",
+ coerced_type = !cir.int<u, 32>,
+ sign_extend = false } ]
+}
+
+module attributes {
+ dlti.dl_spec = #dlti.dl_spec<
+ #dlti.dl_entry<i16, dense<16>: vector<2xi64>>,
+ #dlti.dl_entry<i32, dense<32>: vector<2xi64>>>
+} {
+
+ cir.func @takes_u16(%arg0: !u16i)
+ attributes { test_classify = #extend_unsigned_arg } {
+ cir.return
+ }
+
+ // CHECK: cir.func{{.*}} @takes_u16(%arg0: !u16i {llvm.zeroext})
+
+ cir.func @caller(%arg0: !u16i)
+ attributes { test_classify = #extend_unsigned_arg } {
+ cir.call @takes_u16(%arg0) : (!u16i) -> ()
+ cir.return
+ }
+
+ // CHECK: cir.call @takes_u16(%arg0) : (!u16i {llvm.zeroext}) -> ()
+
+}
More information about the cfe-commits
mailing list