[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