[llvm] /home/macaronjaune/afs/gsoc2026/debian-lsp (PR #210541)
Lucas Ly Ba via llvm-commits
llvm-commits at lists.llvm.org
Sat Jul 18 13:25:25 PDT 2026
https://github.com/lucasly-ba created https://github.com/llvm/llvm-project/pull/210541
A digit-count idiom like ceil(active_bits(x) / 4) guards a ctlz against zero:
select (x == 0), 0, trunc((C - ctlz(x, true)) >> K)
If M <=u C <u M + 2^K (M is the bit width of x), then at x == 0 we get ctlz == M and (C - M) >> K == 0, which is the same as the zero arm. So the compare and select are redundant and the ctlz no longer has to poison on zero:
trunc((C - ctlz(x, false)) >> K)
Fixes #202078
>From 1baef18ae6d6a86ec421d43b6ca02308d63f9665 Mon Sep 17 00:00:00 2001
From: Lucas Ly Ba <hi at lucaslyba.com>
Date: Sat, 18 Jul 2026 18:51:31 +0200
Subject: [PATCH] [InstCombine] Fold zero-guarded biased-shifted ctlz to drop
the zero check
A digit-count idiom like ceil(active_bits(x) / 4) guards a ctlz against zero:
select (x == 0), 0, trunc((C - ctlz(x, true)) >> K)
If M <=u C <u M + 2^K (M is the bit width of x), then at x == 0 we get
ctlz == M and (C - M) >> K == 0, which is the same as the zero arm. So the
compare and select are redundant and the ctlz no longer has to poison on zero:
trunc((C - ctlz(x, false)) >> K)
Fixes #202078
---
flake.lock | 27 ++++
flake.nix | 82 +++++++++++
.../InstCombine/InstCombineSelect.cpp | 64 ++++++++
.../InstCombine/select-ctlz-biased-shift.ll | 139 ++++++++++++++++++
4 files changed, 312 insertions(+)
create mode 100644 flake.lock
create mode 100644 flake.nix
create mode 100644 llvm/test/Transforms/InstCombine/select-ctlz-biased-shift.ll
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000000..f428ce80fc7b5
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1767313136,
+ "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000000..21a44a4dc45b2
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,82 @@
+{
+ description = "llvm-project — Clang Static Analyzer dev shell (NixOS)";
+
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
+
+ outputs =
+ { self, nixpkgs }:
+ let
+ system = "x86_64-linux";
+ pkgs = nixpkgs.legacyPackages.${system};
+
+ # CMake find_package links zlib/zstd/libstdc++ by absolute nix-store path
+ # without embedding an rpath, so the freshly-built llvm-min-tblgen / clang
+ # fail mid-build with "libz.so.1" / "libstdc++.so.6: cannot open shared
+ # object file". Preload them via LD_LIBRARY_PATH. Note: stdenv.cc.cc.lib
+ # (the default gcc stdenv) provides libstdc++.so.6 — clangStdenv's does not.
+ runtimeLibs = with pkgs; [
+ zlib
+ zstd
+ libxml2
+ ncurses
+ libffi
+ libedit
+ stdenv.cc.cc.lib
+ ];
+ in
+ {
+ # Build clang + clang-tools-extra (Clang Static Analyzer) with clangStdenv.
+ devShells.${system}.default = (pkgs.mkShell.override { stdenv = pkgs.clangStdenv; }) {
+ nativeBuildInputs = with pkgs; [
+ cmake
+ ninja
+ python3
+ git
+ lld
+ sccache
+ ];
+ buildInputs = runtimeLibs;
+
+ shellHook = ''
+ export LLVM_SRC="$PWD"
+ export CSA_BUILD="''${CSA_BUILD:-$LLVM_SRC/build/csa}"
+ export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}:''${LD_LIBRARY_PATH:-}"
+
+ csa-configure() {
+ cmake -G Ninja -S "$LLVM_SRC/llvm" -B "$CSA_BUILD" \
+ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DLLVM_ENABLE_ASSERTIONS=ON \
+ -DLLVM_TARGETS_TO_BUILD=X86 \
+ -DLLVM_USE_LINKER=lld \
+ -DLLVM_OPTIMIZED_TABLEGEN=ON \
+ -DBUILD_SHARED_LIBS=ON \
+ -DCMAKE_C_COMPILER_LAUNCHER=sccache \
+ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache \
+ "$@"
+ }
+
+ csa-build() {
+ if [ $# -eq 0 ]; then
+ ninja -C "$CSA_BUILD" clang clang-tidy
+ else
+ ninja -C "$CSA_BUILD" "$@"
+ fi
+ }
+
+ csa-check() {
+ if [ $# -eq 0 ]; then
+ ninja -C "$CSA_BUILD" check-clang-analysis
+ else
+ ninja -C "$CSA_BUILD" "$@"
+ fi
+ }
+
+ export -f csa-configure csa-build csa-check
+
+ echo "CSA dev shell — csa-configure | csa-build [targets] | csa-check [targets]"
+ echo " build dir: $CSA_BUILD"
+ '';
+ };
+ };
+}
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
index f107c15304d9f..e9f24cba66ddf 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
@@ -1602,6 +1602,67 @@ static Value *foldSelectCttzCtlz(ICmpInst *ICI, Value *TrueVal, Value *FalseVal,
return nullptr;
}
+/// Fold the ctlz "digit count" idiom, e.g. ceil(active_bits(x) / 4):
+
+/// select (X == 0), 0, trunc((C - ctlz(X, /*is_zero_poison=*/true)) >> K)
+/// -> trunc((C - ctlz(X, /*is_zero_poison=*/false)) >> K)
+
+/// If M <=u C <u M + 2^K (M is the bit width of X), then at X == 0 we get
+/// ctlz == M and (C - M) >> K == 0, which is the same as the zero arm. So the
+/// select is redundant and the ctlz no longer has to poison on zero.
+/// foldSelectCttzCtlz already does the direct shape; this handles it with the
+/// extra arithmetic in between.
+static Value *foldSelectCtlzBiasedShift(ICmpInst *ICI, Value *TrueVal,
+ Value *FalseVal, InstCombinerImpl &IC) {
+ if (!ICI->isEquality())
+ return nullptr;
+
+ Value *CmpLHS = ICI->getOperand(0), *CmpRHS = ICI->getOperand(1);
+ Value *X;
+ if (match(CmpRHS, m_Zero()))
+ X = CmpLHS;
+ else if (match(CmpLHS, m_Zero()))
+ X = CmpRHS;
+ else
+ return nullptr;
+
+ Value *SelectArg = FalseVal, *ValueOnZero = TrueVal;
+ if (ICI->getPredicate() == ICmpInst::ICMP_NE)
+ std::swap(SelectArg, ValueOnZero);
+
+ if (!match(ValueOnZero, m_Zero()))
+ return nullptr;
+
+ Value *Shifted = SelectArg;
+ bool HasTrunc = match(SelectArg, m_Trunc(m_Value(Shifted)));
+ if (HasTrunc && !SelectArg->hasOneUse())
+ return nullptr;
+
+ const APInt *C, *K;
+ Value *CtlzV;
+ if (!match(Shifted, m_OneUse(m_LShr(
+ m_OneUse(m_Sub(m_APInt(C), m_Value(CtlzV))),
+ m_APInt(K)))))
+ return nullptr;
+ if (!match(CtlzV, m_OneUse(m_Ctlz(m_Specific(X), m_Value()))))
+ return nullptr;
+
+ unsigned M = X->getType()->getScalarSizeInBits();
+ if (K->uge(M) || C->ult(APInt(M, M)))
+ return nullptr;
+ if ((*C - M).uge(APInt::getOneBitSet(M, K->getZExtValue())))
+ return nullptr;
+
+ Value *NewCtlz = IC.Builder.CreateBinaryIntrinsic(Intrinsic::ctlz, X,
+ IC.Builder.getFalse());
+ Value *V = IC.Builder.CreateLShr(
+ IC.Builder.CreateSub(ConstantInt::get(X->getType(), *C), NewCtlz),
+ ConstantInt::get(X->getType(), *K));
+ if (HasTrunc)
+ V = IC.Builder.CreateTrunc(V, SelectArg->getType());
+ return V;
+}
+
static Value *canonicalizeSPF(ICmpInst &Cmp, Value *TrueVal, Value *FalseVal,
InstCombinerImpl &IC) {
Value *LHS, *RHS;
@@ -2466,6 +2527,9 @@ Instruction *InstCombinerImpl::foldSelectInstWithICmp(SelectInst &SI,
if (Value *V = foldSelectCttzCtlz(ICI, TrueVal, FalseVal, *this))
return replaceInstUsesWith(SI, V);
+ if (Value *V = foldSelectCtlzBiasedShift(ICI, TrueVal, FalseVal, *this))
+ return replaceInstUsesWith(SI, V);
+
if (Value *V = canonicalizeSaturatedSubtract(ICI, TrueVal, FalseVal, Builder))
return replaceInstUsesWith(SI, V);
diff --git a/llvm/test/Transforms/InstCombine/select-ctlz-biased-shift.ll b/llvm/test/Transforms/InstCombine/select-ctlz-biased-shift.ll
new file mode 100644
index 0000000000000..d1090c56b0ef2
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/select-ctlz-biased-shift.ll
@@ -0,0 +1,139 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+declare i64 @llvm.ctlz.i64(i64, i1)
+declare i32 @llvm.ctlz.i32(i32, i1)
+declare <2 x i32> @llvm.ctlz.v2i32(<2 x i32>, i1)
+
+define i32 @i64_trunc_to_i32(i64 %x) {
+; CHECK-LABEL: @i64_trunc_to_i32(
+; CHECK-NEXT: [[TMP1:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[TMP2:%.*]] = sub nuw nsw i64 67, [[TMP1]]
+; CHECK-NEXT: [[TMP3:%.*]] = lshr i64 [[TMP2]], 2
+; CHECK-NEXT: [[SEL:%.*]] = trunc nuw i64 [[TMP3]] to i32
+; CHECK-NEXT: ret i32 [[SEL]]
+;
+ %iszero = icmp eq i64 %x, 0
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
+ %biased = sub i64 67, %lz
+ %shifted = lshr i64 %biased, 2
+ %digits = trunc i64 %shifted to i32
+ %sel = select i1 %iszero, i32 0, i32 %digits
+ ret i32 %sel
+}
+
+define i32 @i32_no_trunc(i32 %x) {
+; CHECK-LABEL: @i32_no_trunc(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw i32 35, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i32 [[BIASED]], 2
+; CHECK-NEXT: ret i32 [[DIGITS]]
+;
+ %iszero = icmp eq i32 %x, 0
+ %lz = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+ %biased = sub i32 35, %lz
+ %digits = lshr i32 %biased, 2
+ %sel = select i1 %iszero, i32 0, i32 %digits
+ ret i32 %sel
+}
+
+define i32 @ne_predicate(i32 %x) {
+; CHECK-LABEL: @ne_predicate(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw i32 35, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i32 [[BIASED]], 2
+; CHECK-NEXT: ret i32 [[DIGITS]]
+;
+ %nz = icmp ne i32 %x, 0
+ %lz = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+ %biased = sub i32 35, %lz
+ %digits = lshr i32 %biased, 2
+ %sel = select i1 %nz, i32 %digits, i32 0
+ ret i32 %sel
+}
+
+define i32 @c_equals_m(i32 %x) {
+; CHECK-LABEL: @c_equals_m(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw i32 32, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i32 [[BIASED]], 2
+; CHECK-NEXT: ret i32 [[DIGITS]]
+;
+ %iszero = icmp eq i32 %x, 0
+ %lz = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+ %biased = sub i32 32, %lz
+ %digits = lshr i32 %biased, 2
+ %sel = select i1 %iszero, i32 0, i32 %digits
+ ret i32 %sel
+}
+
+define <2 x i32> @vector(<2 x i32> %x) {
+; CHECK-LABEL: @vector(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i32 0, 33) <2 x i32> @llvm.ctlz.v2i32(<2 x i32> [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw <2 x i32> splat (i32 35), [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr <2 x i32> [[BIASED]], splat (i32 2)
+; CHECK-NEXT: ret <2 x i32> [[DIGITS]]
+;
+ %iszero = icmp eq <2 x i32> %x, zeroinitializer
+ %lz = call <2 x i32> @llvm.ctlz.v2i32(<2 x i32> %x, i1 true)
+ %biased = sub <2 x i32> splat (i32 35), %lz
+ %digits = lshr <2 x i32> %biased, splat (i32 2)
+ %sel = select <2 x i1> %iszero, <2 x i32> zeroinitializer, <2 x i32> %digits
+ ret <2 x i32> %sel
+}
+
+; negative test - C is out of range
+define i32 @negative_c_out_of_range(i64 %x) {
+; CHECK-LABEL: @negative_c_out_of_range(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw i64 100, [[LZ]]
+; CHECK-NEXT: [[SHIFTED:%.*]] = lshr i64 [[BIASED]], 2
+; CHECK-NEXT: [[DIGITS:%.*]] = trunc nuw nsw i64 [[SHIFTED]] to i32
+; CHECK-NEXT: [[SEL:%.*]] = select i1 [[ISZERO]], i32 0, i32 [[DIGITS]]
+; CHECK-NEXT: ret i32 [[SEL]]
+;
+ %iszero = icmp eq i64 %x, 0
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
+ %biased = sub i64 100, %lz
+ %shifted = lshr i64 %biased, 2
+ %digits = trunc i64 %shifted to i32
+ %sel = select i1 %iszero, i32 0, i32 %digits
+ ret i32 %sel
+}
+
+; negative test - nonzero value on the zero arm
+define i32 @negative_nonzero_arm(i32 %x) {
+; CHECK-LABEL: @negative_nonzero_arm(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i32 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X]], i1 true)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw i32 35, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i32 [[BIASED]], 2
+; CHECK-NEXT: [[SEL:%.*]] = select i1 [[ISZERO]], i32 7, i32 [[DIGITS]]
+; CHECK-NEXT: ret i32 [[SEL]]
+;
+ %iszero = icmp eq i32 %x, 0
+ %lz = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+ %biased = sub i32 35, %lz
+ %digits = lshr i32 %biased, 2
+ %sel = select i1 %iszero, i32 7, i32 %digits
+ ret i32 %sel
+}
+
+; negative test - ctlz of a different value
+define i32 @negative_wrong_input(i32 %x, i32 %y) {
+; CHECK-LABEL: @negative_wrong_input(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i32 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[Y:%.*]], i1 true)
+; CHECK-NEXT: [[BIASED:%.*]] = sub nuw nsw i32 35, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i32 [[BIASED]], 2
+; CHECK-NEXT: [[SEL:%.*]] = select i1 [[ISZERO]], i32 0, i32 [[DIGITS]]
+; CHECK-NEXT: ret i32 [[SEL]]
+;
+ %iszero = icmp eq i32 %x, 0
+ %lz = call i32 @llvm.ctlz.i32(i32 %y, i1 true)
+ %biased = sub i32 35, %lz
+ %digits = lshr i32 %biased, 2
+ %sel = select i1 %iszero, i32 0, i32 %digits
+ ret i32 %sel
+}
More information about the llvm-commits
mailing list