[clang] [CIR] Fix __builtin_clz/__builtin_ctz poison_zero to respect target (PR #192865)
via cfe-commits
cfe-commits at lists.llvm.org
Sun Apr 19 12:04:34 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clangir
Author: Henrich Lauko (xlauko)
<details>
<summary>Changes</summary>
CIR was hardcoding poisonZero=true for all clz/ctz builtins, ignoring
the target's isCLZForZeroUndef(). This caused incorrect UB on targets
like AArch64 where clz/ctz of zero is well-defined.
Also add support for __builtin_c[lt]zg fallback (2-arg) variants with
compare+select, and add NYI stubs for elementwise variants.
---
Full diff: https://github.com/llvm/llvm-project/pull/192865.diff
2 Files Affected:
- (modified) clang/lib/CIR/CodeGen/CIRGenBuiltin.cpp (+45-19)
- (added) clang/test/CIR/CodeGenBuiltins/builtin-bit-clz-ctz-target.cpp (+104)
``````````diff
diff --git a/clang/lib/CIR/CodeGen/CIRGenBuiltin.cpp b/clang/lib/CIR/CodeGen/CIRGenBuiltin.cpp
index 1a6aeef73fb79..277f9c9b66eeb 100644
--- a/clang/lib/CIR/CodeGen/CIRGenBuiltin.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenBuiltin.cpp
@@ -40,26 +40,48 @@ static RValue emitLibraryCall(CIRGenFunction &cgf, const FunctionDecl *fd,
}
template <typename Op>
-static RValue emitBuiltinBitOp(CIRGenFunction &cgf, const CallExpr *e,
- bool poisonZero = false) {
+static RValue emitBuiltinBitOp(CIRGenFunction &cgf, const CallExpr *e) {
+ mlir::Value arg = cgf.emitScalarExpr(e->getArg(0));
+ CIRGenBuilderTy &builder = cgf.getBuilder();
+ mlir::Location loc = cgf.getLoc(e->getSourceRange());
+
+ auto op = Op::create(builder, loc, arg);
+ mlir::Value result = op.getResult();
+ mlir::Type resultTy = cgf.convertType(e->getType());
+ if (resultTy != result.getType())
+ result = builder.createIntCast(result, resultTy);
+ return RValue::get(result);
+}
+
+/// Emit a clz/ctz bit op with poison_zero semantics and optional fallback.
+/// When a fallback is present, the result is the fallback value if the input is
+/// zero, otherwise the bit count.
+template <typename Op>
+static RValue emitBuiltinBitOpWithFallback(CIRGenFunction &cgf,
+ const CallExpr *e) {
assert(!cir::MissingFeatures::builtinCheckKind());
+ bool hasFallback = e->getNumArgs() > 1;
+ bool poisonZero = hasFallback || cgf.getTarget().isCLZForZeroUndef();
+
mlir::Value arg = cgf.emitScalarExpr(e->getArg(0));
CIRGenBuilderTy &builder = cgf.getBuilder();
+ mlir::Location loc = cgf.getLoc(e->getSourceRange());
- Op op;
- if constexpr (std::is_same_v<Op, cir::BitClzOp> ||
- std::is_same_v<Op, cir::BitCtzOp>)
- op = Op::create(builder, cgf.getLoc(e->getSourceRange()), arg, poisonZero);
- else
- op = Op::create(builder, cgf.getLoc(e->getSourceRange()), arg);
-
+ auto op = Op::create(builder, loc, arg, poisonZero);
mlir::Value result = op.getResult();
- mlir::Type exprTy = cgf.convertType(e->getType());
- if (exprTy != result.getType())
- result = builder.createIntCast(result, exprTy);
+ mlir::Type resultTy = cgf.convertType(e->getType());
+ if (resultTy != result.getType())
+ result = builder.createIntCast(result, resultTy);
- return RValue::get(result);
+ if (!hasFallback)
+ return RValue::get(result);
+
+ mlir::Value zero = builder.getNullValue(arg.getType(), loc);
+ mlir::Value isZero =
+ builder.createCompare(loc, cir::CmpOpKind::eq, arg, zero);
+ mlir::Value fallbackValue = cgf.emitScalarExpr(e->getArg(1));
+ return RValue::get(builder.createSelect(loc, isZero, fallbackValue, result));
}
/// Emit the conversions required to turn the given value into an
@@ -1117,16 +1139,21 @@ RValue CIRGenFunction::emitBuiltinExpr(const GlobalDecl &gd, unsigned builtinID,
case Builtin::BI__builtin_ctzl:
case Builtin::BI__builtin_ctzll:
case Builtin::BI__builtin_ctzg:
- assert(!cir::MissingFeatures::builtinCheckKind());
- return emitBuiltinBitOp<cir::BitCtzOp>(*this, e, /*poisonZero=*/true);
+ return emitBuiltinBitOpWithFallback<cir::BitCtzOp>(*this, e);
case Builtin::BI__builtin_clzs:
case Builtin::BI__builtin_clz:
case Builtin::BI__builtin_clzl:
case Builtin::BI__builtin_clzll:
case Builtin::BI__builtin_clzg:
- assert(!cir::MissingFeatures::builtinCheckKind());
- return emitBuiltinBitOp<cir::BitClzOp>(*this, e, /*poisonZero=*/true);
+ return emitBuiltinBitOpWithFallback<cir::BitClzOp>(*this, e);
+
+ case Builtin::BI__builtin_elementwise_ctzg:
+ cgm.errorNYI(e->getSourceRange(), "__builtin_elementwise_ctzg");
+ return RValue::get(nullptr);
+ case Builtin::BI__builtin_elementwise_clzg:
+ cgm.errorNYI(e->getSourceRange(), "__builtin_elementwise_clzg");
+ return RValue::get(nullptr);
case Builtin::BI__builtin_ffs:
case Builtin::BI__builtin_ffsl:
@@ -1141,8 +1168,7 @@ RValue CIRGenFunction::emitBuiltinExpr(const GlobalDecl &gd, unsigned builtinID,
case Builtin::BI__lzcnt16:
case Builtin::BI__lzcnt:
case Builtin::BI__lzcnt64:
- assert(!cir::MissingFeatures::builtinCheckKind());
- return emitBuiltinBitOp<cir::BitClzOp>(*this, e, /*poisonZero=*/false);
+ return emitBuiltinBitOp<cir::BitClzOp>(*this, e);
case Builtin::BI__popcnt16:
case Builtin::BI__popcnt:
diff --git a/clang/test/CIR/CodeGenBuiltins/builtin-bit-clz-ctz-target.cpp b/clang/test/CIR/CodeGenBuiltins/builtin-bit-clz-ctz-target.cpp
new file mode 100644
index 0000000000000..0c922b0cca712
--- /dev/null
+++ b/clang/test/CIR/CodeGenBuiltins/builtin-bit-clz-ctz-target.cpp
@@ -0,0 +1,104 @@
+// Tests that __builtin_clz/__builtin_ctz respect isCLZForZeroUndef() per target.
+// x86 returns true (default), AArch64 returns false.
+
+// RUN: %clang_cc1 -triple aarch64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s -check-prefix=AARCH64-CIR
+// RUN: %clang_cc1 -triple aarch64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --input-file=%t-cir.ll %s -check-prefix=AARCH64-LLVM
+// RUN: %clang_cc1 -triple aarch64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s -check-prefix=AARCH64-LLVM
+
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s -check-prefix=X86-CIR
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --input-file=%t-cir.ll %s -check-prefix=X86-LLVM
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s -check-prefix=X86-LLVM
+
+int test_builtin_ctz(unsigned x) {
+ return __builtin_ctz(x);
+}
+
+// AARCH64-CIR-LABEL: _Z16test_builtin_ctzj
+// AARCH64-CIR: cir.ctz %{{.+}} : !u32i
+// AARCH64-CIR-NOT: poison_zero
+
+// AARCH64-LLVM-LABEL: _Z16test_builtin_ctzj
+// AARCH64-LLVM: %{{.+}} = call i32 @llvm.cttz.i32(i32 %{{.+}}, i1 false)
+
+// X86-CIR-LABEL: _Z16test_builtin_ctzj
+// X86-CIR: cir.ctz %{{.+}} poison_zero : !u32i
+
+// X86-LLVM-LABEL: _Z16test_builtin_ctzj
+// X86-LLVM: %{{.+}} = call i32 @llvm.cttz.i32(i32 %{{.+}}, i1 true)
+
+int test_builtin_clz(unsigned x) {
+ return __builtin_clz(x);
+}
+
+// AARCH64-CIR-LABEL: _Z16test_builtin_clzj
+// AARCH64-CIR: cir.clz %{{.+}} : !u32i
+// AARCH64-CIR-NOT: poison_zero
+
+// AARCH64-LLVM-LABEL: _Z16test_builtin_clzj
+// AARCH64-LLVM: %{{.+}} = call i32 @llvm.ctlz.i32(i32 %{{.+}}, i1 false)
+
+// X86-CIR-LABEL: _Z16test_builtin_clzj
+// X86-CIR: cir.clz %{{.+}} poison_zero : !u32i
+
+// X86-LLVM-LABEL: _Z16test_builtin_clzj
+// X86-LLVM: %{{.+}} = call i32 @llvm.ctlz.i32(i32 %{{.+}}, i1 true)
+
+int test_builtin_ctzg_fallback(unsigned x, int fb) {
+ return __builtin_ctzg(x, fb);
+}
+
+// On both targets, the fallback case always uses poison_zero=true.
+
+// AARCH64-CIR-LABEL: _Z26test_builtin_ctzg_fallbackji
+// AARCH64-CIR: %[[CTZ:.+]] = cir.ctz %{{.+}} poison_zero : !u32i
+// AARCH64-CIR: %[[ZERO:.+]] = cir.const #cir.int<0>
+// AARCH64-CIR: %[[ISZERO:.+]] = cir.cmp eq %{{.+}}, %[[ZERO]] : !u32i
+// AARCH64-CIR: cir.select if %[[ISZERO]]
+
+// AARCH64-LLVM-LABEL: _Z26test_builtin_ctzg_fallbackji
+// AARCH64-LLVM: %[[CTZ:.+]] = call i32 @llvm.cttz.i32(i32 %{{.+}}, i1 true)
+// AARCH64-LLVM: %[[ISZERO:.+]] = icmp eq i32 %{{.+}}, 0
+// AARCH64-LLVM: select i1 %[[ISZERO]], i32 %{{.+}}, i32 %[[CTZ]]
+
+// X86-CIR-LABEL: _Z26test_builtin_ctzg_fallbackji
+// X86-CIR: %[[CTZ:.+]] = cir.ctz %{{.+}} poison_zero : !u32i
+// X86-CIR: %[[ZERO:.+]] = cir.const #cir.int<0>
+// X86-CIR: %[[ISZERO:.+]] = cir.cmp eq %{{.+}}, %[[ZERO]] : !u32i
+// X86-CIR: cir.select if %[[ISZERO]]
+
+// X86-LLVM-LABEL: _Z26test_builtin_ctzg_fallbackji
+// X86-LLVM: %[[CTZ:.+]] = call i32 @llvm.cttz.i32(i32 %{{.+}}, i1 true)
+// X86-LLVM: %[[ISZERO:.+]] = icmp eq i32 %{{.+}}, 0
+// X86-LLVM: select i1 %[[ISZERO]], i32 %{{.+}}, i32 %[[CTZ]]
+
+int test_builtin_clzg_fallback(unsigned x, int fb) {
+ return __builtin_clzg(x, fb);
+}
+
+// AARCH64-CIR-LABEL: _Z26test_builtin_clzg_fallbackji
+// AARCH64-CIR: %[[CLZ:.+]] = cir.clz %{{.+}} poison_zero : !u32i
+// AARCH64-CIR: %[[ZERO:.+]] = cir.const #cir.int<0>
+// AARCH64-CIR: %[[ISZERO:.+]] = cir.cmp eq %{{.+}}, %[[ZERO]] : !u32i
+// AARCH64-CIR: cir.select if %[[ISZERO]]
+
+// AARCH64-LLVM-LABEL: _Z26test_builtin_clzg_fallbackji
+// AARCH64-LLVM: %[[CLZ:.+]] = call i32 @llvm.ctlz.i32(i32 %{{.+}}, i1 true)
+// AARCH64-LLVM: %[[ISZERO:.+]] = icmp eq i32 %{{.+}}, 0
+// AARCH64-LLVM: select i1 %[[ISZERO]], i32 %{{.+}}, i32 %[[CLZ]]
+
+// X86-CIR-LABEL: _Z26test_builtin_clzg_fallbackji
+// X86-CIR: %[[CLZ:.+]] = cir.clz %{{.+}} poison_zero : !u32i
+// X86-CIR: %[[ZERO:.+]] = cir.const #cir.int<0>
+// X86-CIR: %[[ISZERO:.+]] = cir.cmp eq %{{.+}}, %[[ZERO]] : !u32i
+// X86-CIR: cir.select if %[[ISZERO]]
+
+// X86-LLVM-LABEL: _Z26test_builtin_clzg_fallbackji
+// X86-LLVM: %[[CLZ:.+]] = call i32 @llvm.ctlz.i32(i32 %{{.+}}, i1 true)
+// X86-LLVM: %[[ISZERO:.+]] = icmp eq i32 %{{.+}}, 0
+// X86-LLVM: select i1 %[[ISZERO]], i32 %{{.+}}, i32 %[[CLZ]]
\ No newline at end of file
``````````
</details>
https://github.com/llvm/llvm-project/pull/192865
More information about the cfe-commits
mailing list