[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