[llvm] [DAGCombiner] Fold bswap of single-byte-known-nonzero value to a shift (PR #193473)
Paweł Bylica via llvm-commits
llvm-commits at lists.llvm.org
Thu Apr 23 01:57:13 PDT 2026
https://github.com/chfast updated https://github.com/llvm/llvm-project/pull/193473
>From 5f9641173cfcc16e40b36cba21dc3b6df88fd7b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 12:36:39 +0200
Subject: [PATCH 1/9] [AArch64,RISCV][test] Add bswap-of-known-one-byte-nonzero
tests (NFC)
Add test coverage for bswap applied to a value where at most one byte
is known to be possibly nonzero (via and-mask or zext from a smaller
type). In this case bswap is equivalent to a shift that moves that
byte to the mirror position, but the DAG combiner does not currently
recognize this and emits mask+rev+shift sequences.
These tests document the current (suboptimal) codegen as a precondition
for a follow-up combiner change.
---
llvm/test/CodeGen/AArch64/bswap-known-bits.ll | 126 ++++++++++-
llvm/test/CodeGen/RISCV/bswap-known-bits.ll | 206 ++++++++++++++++++
2 files changed, 328 insertions(+), 4 deletions(-)
create mode 100644 llvm/test/CodeGen/RISCV/bswap-known-bits.ll
diff --git a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
index 23619e47367d0..e7c32a5beea41 100644
--- a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
@@ -8,7 +8,7 @@ declare i64 @llvm.bswap.i64(i64)
define i1 @test1(i16 %arg) {
; CHECK-LABEL: test1:
; CHECK: ; %bb.0:
-; CHECK-NEXT: mov w0, #1
+; CHECK-NEXT: mov w0, #1 ; =0x1
; CHECK-NEXT: ret
%a = or i16 %arg, 511
%b = call i16 @llvm.bswap.i16(i16 %a)
@@ -20,7 +20,7 @@ define i1 @test1(i16 %arg) {
define i1 @test2(i16 %arg) {
; CHECK-LABEL: test2:
; CHECK: ; %bb.0:
-; CHECK-NEXT: mov w0, #1
+; CHECK-NEXT: mov w0, #1 ; =0x1
; CHECK-NEXT: ret
%a = or i16 %arg, 1
%b = call i16 @llvm.bswap.i16(i16 %a)
@@ -32,7 +32,7 @@ define i1 @test2(i16 %arg) {
define i1 @test3(i16 %arg) {
; CHECK-LABEL: test3:
; CHECK: ; %bb.0:
-; CHECK-NEXT: mov w0, #1
+; CHECK-NEXT: mov w0, #1 ; =0x1
; CHECK-NEXT: ret
%a = or i16 %arg, 256
%b = call i16 @llvm.bswap.i16(i16 %a)
@@ -44,7 +44,7 @@ define i1 @test3(i16 %arg) {
define i1 @test4(i32 %arg) {
; CHECK-LABEL: test4:
; CHECK: ; %bb.0:
-; CHECK-NEXT: mov w0, #1
+; CHECK-NEXT: mov w0, #1 ; =0x1
; CHECK-NEXT: ret
%a = or i32 %arg, 2147483647 ; i32_MAX
%b = call i32 @llvm.bswap.i32(i32 %a)
@@ -112,3 +112,121 @@ define void @demand_one_loaded_byte(ptr %xp, ptr %yp) {
store i32 %r, ptr %yp, align 4
ret void
}
+
+; Source-side missed optimizations: when the bswap *operand* is known to have
+; at most one byte of possibly-nonzero bits, the bswap is equivalent to a
+; shift moving that byte to the mirror position. Today these emit
+; rev + shift + mask; after the generic DAGCombiner fold lands, they should
+; become a single shift (or a no-op for i64 middle bytes that don't exist).
+
+define i16 @bswap_src_and_lo_i16(i16 %x) {
+; CHECK-LABEL: bswap_src_and_lo_i16:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff
+; CHECK-NEXT: rev16 w0, w8
+; CHECK-NEXT: ret
+ %m = and i16 %x, 255
+ %b = call i16 @llvm.bswap.i16(i16 %m)
+ ret i16 %b
+}
+
+define i16 @bswap_src_and_hi_i16(i16 %x) {
+; CHECK-LABEL: bswap_src_and_hi_i16:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xffffff00
+; CHECK-NEXT: rev16 w0, w8
+; CHECK-NEXT: ret
+ %m = and i16 %x, 65280
+ %b = call i16 @llvm.bswap.i16(i16 %m)
+ ret i16 %b
+}
+
+define i16 @bswap_src_zext_i8_to_i16(i8 %x) {
+; CHECK-LABEL: bswap_src_zext_i8_to_i16:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff
+; CHECK-NEXT: rev16 w0, w8
+; CHECK-NEXT: ret
+ %z = zext i8 %x to i16
+ %b = call i16 @llvm.bswap.i16(i16 %z)
+ ret i16 %b
+}
+
+define i32 @bswap_src_and_byte0_i32(i32 %x) {
+; CHECK-LABEL: bswap_src_and_byte0_i32:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff
+; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: ret
+ %m = and i32 %x, 255
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_and_byte1_i32(i32 %x) {
+; CHECK-LABEL: bswap_src_and_byte1_i32:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff00
+; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: ret
+ %m = and i32 %x, 65280
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_and_byte2_i32(i32 %x) {
+; CHECK-LABEL: bswap_src_and_byte2_i32:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff0000
+; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: ret
+ %m = and i32 %x, 16711680
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_and_byte3_i32(i32 %x) {
+; CHECK-LABEL: bswap_src_and_byte3_i32:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff000000
+; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: ret
+ %m = and i32 %x, 4278190080
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_zext_i8_to_i32(i8 %x) {
+; CHECK-LABEL: bswap_src_zext_i8_to_i32:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: and w8, w0, #0xff
+; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: ret
+ %z = zext i8 %x to i32
+ %b = call i32 @llvm.bswap.i32(i32 %z)
+ ret i32 %b
+}
+
+define i64 @bswap_src_zext_i8_to_i64(i8 %x) {
+; CHECK-LABEL: bswap_src_zext_i8_to_i64:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: ; kill: def $w0 killed $w0 def $x0
+; CHECK-NEXT: and x8, x0, #0xff
+; CHECK-NEXT: rev x0, x8
+; CHECK-NEXT: ret
+ %z = zext i8 %x to i64
+ %b = call i64 @llvm.bswap.i64(i64 %z)
+ ret i64 %b
+}
+
+define i64 @bswap_src_zext_i16_to_i64(i16 %x) {
+; CHECK-LABEL: bswap_src_zext_i16_to_i64:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: ; kill: def $w0 killed $w0 def $x0
+; CHECK-NEXT: and x8, x0, #0xffff
+; CHECK-NEXT: rev x0, x8
+; CHECK-NEXT: ret
+ %z = zext i16 %x to i64
+ %b = call i64 @llvm.bswap.i64(i64 %z)
+ ret i64 %b
+}
diff --git a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
new file mode 100644
index 0000000000000..ebca0e4acbc2c
--- /dev/null
+++ b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
@@ -0,0 +1,206 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
+; RUN: llc -mtriple=riscv32 -mattr=+zbb -verify-machineinstrs < %s \
+; RUN: | FileCheck %s -check-prefixes=RV32ZB
+; RUN: llc -mtriple=riscv64 -mattr=+zbb -verify-machineinstrs < %s \
+; RUN: | FileCheck %s -check-prefixes=RV64ZB
+
+; Source-side missed optimizations: when the bswap *operand* is known to have
+; at most one byte of possibly-nonzero bits, the bswap is equivalent to a
+; shift moving that byte to the mirror position. Today these emit
+; mask + rev8 + shift; after the generic DAGCombiner fold lands, they should
+; become a single shift.
+
+declare i16 @llvm.bswap.i16(i16)
+declare i32 @llvm.bswap.i32(i32)
+declare i64 @llvm.bswap.i64(i64)
+
+define i16 @bswap_src_and_lo_i16(i16 %x) {
+; RV32ZB-LABEL: bswap_src_and_lo_i16:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: zext.b a0, a0
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: srli a0, a0, 16
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_and_lo_i16:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: zext.b a0, a0
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 48
+; RV64ZB-NEXT: ret
+ %m = and i16 %x, 255
+ %b = call i16 @llvm.bswap.i16(i16 %m)
+ ret i16 %b
+}
+
+define i16 @bswap_src_and_hi_i16(i16 %x) {
+; RV32ZB-LABEL: bswap_src_and_hi_i16:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: andi a0, a0, -256
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: srli a0, a0, 16
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_and_hi_i16:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: andi a0, a0, -256
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 48
+; RV64ZB-NEXT: ret
+ %m = and i16 %x, 65280
+ %b = call i16 @llvm.bswap.i16(i16 %m)
+ ret i16 %b
+}
+
+define i16 @bswap_src_zext_i8_to_i16(i8 %x) {
+; RV32ZB-LABEL: bswap_src_zext_i8_to_i16:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: zext.b a0, a0
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: srli a0, a0, 16
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_zext_i8_to_i16:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: zext.b a0, a0
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 48
+; RV64ZB-NEXT: ret
+ %z = zext i8 %x to i16
+ %b = call i16 @llvm.bswap.i16(i16 %z)
+ ret i16 %b
+}
+
+define i32 @bswap_src_and_byte0_i32(i32 %x) {
+; RV32ZB-LABEL: bswap_src_and_byte0_i32:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: zext.b a0, a0
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_and_byte0_i32:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: zext.b a0, a0
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: ret
+ %m = and i32 %x, 255
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_and_byte1_i32(i32 %x) {
+; RV32ZB-LABEL: bswap_src_and_byte1_i32:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: lui a1, 16
+; RV32ZB-NEXT: addi a1, a1, -256
+; RV32ZB-NEXT: and a0, a0, a1
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_and_byte1_i32:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: lui a1, 16
+; RV64ZB-NEXT: addi a1, a1, -256
+; RV64ZB-NEXT: and a0, a0, a1
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: ret
+ %m = and i32 %x, 65280
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_and_byte2_i32(i32 %x) {
+; RV32ZB-LABEL: bswap_src_and_byte2_i32:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: lui a1, 4080
+; RV32ZB-NEXT: and a0, a0, a1
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_and_byte2_i32:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: lui a1, 4080
+; RV64ZB-NEXT: and a0, a0, a1
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: ret
+ %m = and i32 %x, 16711680
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_and_byte3_i32(i32 %x) {
+; RV32ZB-LABEL: bswap_src_and_byte3_i32:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: lui a1, 1044480
+; RV32ZB-NEXT: and a0, a0, a1
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_and_byte3_i32:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: lui a1, 1044480
+; RV64ZB-NEXT: and a0, a0, a1
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: ret
+ %m = and i32 %x, 4278190080
+ %b = call i32 @llvm.bswap.i32(i32 %m)
+ ret i32 %b
+}
+
+define i32 @bswap_src_zext_i8_to_i32(i8 %x) {
+; RV32ZB-LABEL: bswap_src_zext_i8_to_i32:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: zext.b a0, a0
+; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_zext_i8_to_i32:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: zext.b a0, a0
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: ret
+ %z = zext i8 %x to i32
+ %b = call i32 @llvm.bswap.i32(i32 %z)
+ ret i32 %b
+}
+
+define i64 @bswap_src_zext_i8_to_i64(i8 %x) {
+; RV32ZB-LABEL: bswap_src_zext_i8_to_i64:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: zext.b a0, a0
+; RV32ZB-NEXT: rev8 a1, a0
+; RV32ZB-NEXT: li a0, 0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_zext_i8_to_i64:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: zext.b a0, a0
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: ret
+ %z = zext i8 %x to i64
+ %b = call i64 @llvm.bswap.i64(i64 %z)
+ ret i64 %b
+}
+
+define i64 @bswap_src_zext_i16_to_i64(i16 %x) {
+; RV32ZB-LABEL: bswap_src_zext_i16_to_i64:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: zext.h a0, a0
+; RV32ZB-NEXT: rev8 a1, a0
+; RV32ZB-NEXT: li a0, 0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_src_zext_i16_to_i64:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: zext.h a0, a0
+; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: ret
+ %z = zext i16 %x to i64
+ %b = call i64 @llvm.bswap.i64(i64 %z)
+ ret i64 %b
+}
>From 1df26ef78341fd1e705cf8807d428d1020c7cf14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 12:48:56 +0200
Subject: [PATCH 2/9] [DAGCombiner] Fold bswap of single-byte-known-nonzero
value to a shift
When computeKnownBits proves that a bswap operand has at most one byte
of possibly-nonzero bits at a known byte-aligned position, the bswap is
equivalent to a shift that moves that byte to the mirror position. This
is a producer-side known-bits rule; it fires in visitBSWAP regardless
of how the narrow-value provenance was established, covering shapes
such as
bswap(and X, 0xFF)
bswap(and X, 0xFF00) ; all byte positions
bswap(zext i8 X to iN)
bswap(zext i16 X to i64)
Motivation. While investigating a RISCV codegen regression under
-combiner-topological-sorting (bswap-shift.ll), I traced the root cause
to the existing consumer-side rule in TargetLowering::SimplifyDemandedBits
for ISD::BSWAP: when a consumer demands only one byte of the bswap
result, that rule rewrites the inner bswap as a shift. Under topological
(producer-first) worklist ordering, this rewrite fires on the inner
bswap of bswap(srl(bswap X, 8)) before visitBSWAP's syntactic pattern
bswap(srl X, 8) -> (bswap X) shl 8 can collapse the whole chain, so
the outer bswap is stranded as bswap(and(X, MASK)), which no existing
combine recognizes.
The new rule is orthogonal: it matches when the operand's known bits
(rather than the consumer's demand) characterize a single-byte value,
so it closes the gap in both topological and non-topological modes. It
also captures genuinely new missed optimizations that are independent
of the ordering problem: bswap(zext i8 -> i64) previously emitted
zext.b + rev8 on RISCV+zbb and is now a single slli 56.
Placement choice. The rule is a natural dual of the existing consumer-
side rule in TargetLowering::SimplifyDemandedBits for ISD::BSWAP; the
needed information (computeKnownBits of the source) is readily
available there. I prototyped that placement and found it strictly
weaker: SimplifyDemandedBits runs later in the pipeline, typically
after integer type promotion, so a rewrite of a native i16 bswap fires
only after the value has been widened to i64 and the outer i16->i64
promotion shifts block further simplification. Placing the rule in
visitBSWAP fires pre-type-legalize on native i16/i32 values, letting
the resulting shift compose cleanly with downstream combines. A
comment in visitBSWAP cross-references the dual rule for future
readers.
Potential extension. The same logic can also be added to
TargetLowering::SimplifyDemandedBits (as a second check after the
existing one-byte-demanded rule, using computeKnownBits(Src) rather
than the demand-restricted Known2) for cases where visitBSWAP is not
reached via the combiner worklist but the bswap is recursed into by a
parent's demand analysis. I did not include that duplicate here
because no pre-existing test exercises such a path; the visitBSWAP
placement alone closes every case I found.
Test updates.
- AArch64/bswap-known-bits.ll and RISCV/bswap-known-bits.ll
(added as a precondition commit) now show the improved single-shift
codegen.
- X86/known-pow2.ll: pow2_bswap and pow2_bitreverse tighten by ~3
instructions each; the bswap of select i32 {4,8} is now a shift,
which the pow2 lowering unfolds further.
Verified on the full X86+AArch64+RISCV CodeGen lit suites (11944/11944
passing, no regressions).
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 22 ++++-
llvm/test/CodeGen/AArch64/bswap-known-bits.ll | 35 ++++---
llvm/test/CodeGen/RISCV/bswap-known-bits.ll | 91 ++++++++++---------
llvm/test/CodeGen/X86/known-pow2.ll | 20 ++--
4 files changed, 99 insertions(+), 69 deletions(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index d1c7e668a2604..5c000412117c5 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12094,9 +12094,11 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
return DAG.getNode(ISD::BITREVERSE, DL, VT, BSwap);
}
+ unsigned BW = VT.getScalarSizeInBits();
+ assert(BW % 16 == 0 && "bswap requires a multiple-of-16-bit width");
+
// fold (bswap shl(x,c)) -> (zext(bswap(trunc(shl(x,sub(c,bw/2))))))
// iff x >= bw/2 (i.e. lower half is known zero)
- unsigned BW = VT.getScalarSizeInBits();
if (BW >= 32 && N0.getOpcode() == ISD::SHL && N0.hasOneUse()) {
auto *ShAmt = dyn_cast<ConstantSDNode>(N0.getOperand(1));
EVT HalfVT = EVT::getIntegerVT(*DAG.getContext(), BW / 2);
@@ -12133,6 +12135,24 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
if (SDValue V = foldBitOrderCrossLogicOp(N, DAG))
return V;
+ // If only one byte of the operand may be nonzero, bswap becomes a shift
+ // to the mirror byte. Producer-side dual of the ISD::BSWAP rule in
+ // TargetLowering::SimplifyDemandedBits; placed here to fire pre-legalize.
+ KnownBits Known = DAG.computeKnownBits(N0);
+ unsigned Lo = Known.countMinTrailingZeros();
+ unsigned Hi = BW - Known.countMinLeadingZeros();
+ if (Lo < Hi && Lo / 8 == (Hi - 1) / 8) {
+ unsigned SrcByte = Lo / 8;
+ unsigned DstByte = BW / 8 - 1 - SrcByte;
+ unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
+ unsigned Amt =
+ (DstByte > SrcByte ? DstByte - SrcByte : SrcByte - DstByte) * 8;
+ SDNodeFlags Flags =
+ Opc == ISD::SHL ? SDNodeFlags::NoUnsignedWrap : SDNodeFlags::Exact;
+ return DAG.getNode(Opc, DL, VT, N0, DAG.getShiftAmountConstant(Amt, VT, DL),
+ Flags);
+ }
+
return SDValue();
}
diff --git a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
index e7c32a5beea41..417db22639ec4 100644
--- a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
@@ -155,8 +155,7 @@ define i16 @bswap_src_zext_i8_to_i16(i8 %x) {
define i32 @bswap_src_and_byte0_i32(i32 %x) {
; CHECK-LABEL: bswap_src_and_byte0_i32:
; CHECK: ; %bb.0:
-; CHECK-NEXT: and w8, w0, #0xff
-; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: lsl w0, w0, #24
; CHECK-NEXT: ret
%m = and i32 %x, 255
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -166,8 +165,8 @@ define i32 @bswap_src_and_byte0_i32(i32 %x) {
define i32 @bswap_src_and_byte1_i32(i32 %x) {
; CHECK-LABEL: bswap_src_and_byte1_i32:
; CHECK: ; %bb.0:
-; CHECK-NEXT: and w8, w0, #0xff00
-; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: lsl w8, w0, #8
+; CHECK-NEXT: and w0, w8, #0xff0000
; CHECK-NEXT: ret
%m = and i32 %x, 65280
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -178,7 +177,7 @@ define i32 @bswap_src_and_byte2_i32(i32 %x) {
; CHECK-LABEL: bswap_src_and_byte2_i32:
; CHECK: ; %bb.0:
; CHECK-NEXT: and w8, w0, #0xff0000
-; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: lsr w0, w8, #8
; CHECK-NEXT: ret
%m = and i32 %x, 16711680
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -188,8 +187,7 @@ define i32 @bswap_src_and_byte2_i32(i32 %x) {
define i32 @bswap_src_and_byte3_i32(i32 %x) {
; CHECK-LABEL: bswap_src_and_byte3_i32:
; CHECK: ; %bb.0:
-; CHECK-NEXT: and w8, w0, #0xff000000
-; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: lsr w0, w0, #24
; CHECK-NEXT: ret
%m = and i32 %x, 4278190080
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -199,8 +197,7 @@ define i32 @bswap_src_and_byte3_i32(i32 %x) {
define i32 @bswap_src_zext_i8_to_i32(i8 %x) {
; CHECK-LABEL: bswap_src_zext_i8_to_i32:
; CHECK: ; %bb.0:
-; CHECK-NEXT: and w8, w0, #0xff
-; CHECK-NEXT: rev w0, w8
+; CHECK-NEXT: lsl w0, w0, #24
; CHECK-NEXT: ret
%z = zext i8 %x to i32
%b = call i32 @llvm.bswap.i32(i32 %z)
@@ -211,8 +208,7 @@ define i64 @bswap_src_zext_i8_to_i64(i8 %x) {
; CHECK-LABEL: bswap_src_zext_i8_to_i64:
; CHECK: ; %bb.0:
; CHECK-NEXT: ; kill: def $w0 killed $w0 def $x0
-; CHECK-NEXT: and x8, x0, #0xff
-; CHECK-NEXT: rev x0, x8
+; CHECK-NEXT: lsl x0, x0, #56
; CHECK-NEXT: ret
%z = zext i8 %x to i64
%b = call i64 @llvm.bswap.i64(i64 %z)
@@ -230,3 +226,20 @@ define i64 @bswap_src_zext_i16_to_i64(i16 %x) {
%b = call i64 @llvm.bswap.i64(i64 %z)
ret i64 %b
}
+
+; Regression: the producer-side combine must not tag the produced shl
+; with nsw. For bswap(and X, 0xFF) on i16 we shift byte 0 into the top
+; byte, which becomes the sign bit. Bit 7 of the input freely flips the
+; result's sign, so nsw would be unsound and make `icmp slt 0` fold to
+; false. Here the comparison must remain: the result depends on bit 7
+; of the input.
+define i1 @bswap_lo_byte_sign_i16(i16 %x) {
+; CHECK-LABEL: bswap_lo_byte_sign_i16:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: ubfx w0, w0, #7, #1
+; CHECK-NEXT: ret
+ %m = and i16 %x, 255
+ %b = call i16 @llvm.bswap.i16(i16 %m)
+ %neg = icmp slt i16 %b, 0
+ ret i1 %neg
+}
diff --git a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
index ebca0e4acbc2c..2e2779938d36b 100644
--- a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
@@ -17,16 +17,12 @@ declare i64 @llvm.bswap.i64(i64)
define i16 @bswap_src_and_lo_i16(i16 %x) {
; RV32ZB-LABEL: bswap_src_and_lo_i16:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: zext.b a0, a0
-; RV32ZB-NEXT: rev8 a0, a0
-; RV32ZB-NEXT: srli a0, a0, 16
+; RV32ZB-NEXT: slli a0, a0, 8
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_and_lo_i16:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: zext.b a0, a0
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 48
+; RV64ZB-NEXT: slli a0, a0, 8
; RV64ZB-NEXT: ret
%m = and i16 %x, 255
%b = call i16 @llvm.bswap.i16(i16 %m)
@@ -36,16 +32,14 @@ define i16 @bswap_src_and_lo_i16(i16 %x) {
define i16 @bswap_src_and_hi_i16(i16 %x) {
; RV32ZB-LABEL: bswap_src_and_hi_i16:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: andi a0, a0, -256
-; RV32ZB-NEXT: rev8 a0, a0
-; RV32ZB-NEXT: srli a0, a0, 16
+; RV32ZB-NEXT: slli a0, a0, 16
+; RV32ZB-NEXT: srli a0, a0, 24
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_and_hi_i16:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: andi a0, a0, -256
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 48
+; RV64ZB-NEXT: slli a0, a0, 48
+; RV64ZB-NEXT: srli a0, a0, 56
; RV64ZB-NEXT: ret
%m = and i16 %x, 65280
%b = call i16 @llvm.bswap.i16(i16 %m)
@@ -55,16 +49,12 @@ define i16 @bswap_src_and_hi_i16(i16 %x) {
define i16 @bswap_src_zext_i8_to_i16(i8 %x) {
; RV32ZB-LABEL: bswap_src_zext_i8_to_i16:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: zext.b a0, a0
-; RV32ZB-NEXT: rev8 a0, a0
-; RV32ZB-NEXT: srli a0, a0, 16
+; RV32ZB-NEXT: slli a0, a0, 8
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_zext_i8_to_i16:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: zext.b a0, a0
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 48
+; RV64ZB-NEXT: slli a0, a0, 8
; RV64ZB-NEXT: ret
%z = zext i8 %x to i16
%b = call i16 @llvm.bswap.i16(i16 %z)
@@ -74,15 +64,12 @@ define i16 @bswap_src_zext_i8_to_i16(i8 %x) {
define i32 @bswap_src_and_byte0_i32(i32 %x) {
; RV32ZB-LABEL: bswap_src_and_byte0_i32:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: zext.b a0, a0
-; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: slli a0, a0, 24
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_and_byte0_i32:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: zext.b a0, a0
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: slliw a0, a0, 24
; RV64ZB-NEXT: ret
%m = and i32 %x, 255
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -95,7 +82,7 @@ define i32 @bswap_src_and_byte1_i32(i32 %x) {
; RV32ZB-NEXT: lui a1, 16
; RV32ZB-NEXT: addi a1, a1, -256
; RV32ZB-NEXT: and a0, a0, a1
-; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: slli a0, a0, 8
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_and_byte1_i32:
@@ -103,8 +90,7 @@ define i32 @bswap_src_and_byte1_i32(i32 %x) {
; RV64ZB-NEXT: lui a1, 16
; RV64ZB-NEXT: addi a1, a1, -256
; RV64ZB-NEXT: and a0, a0, a1
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: slli a0, a0, 8
; RV64ZB-NEXT: ret
%m = and i32 %x, 65280
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -116,15 +102,14 @@ define i32 @bswap_src_and_byte2_i32(i32 %x) {
; RV32ZB: # %bb.0:
; RV32ZB-NEXT: lui a1, 4080
; RV32ZB-NEXT: and a0, a0, a1
-; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: srli a0, a0, 8
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_and_byte2_i32:
; RV64ZB: # %bb.0:
; RV64ZB-NEXT: lui a1, 4080
; RV64ZB-NEXT: and a0, a0, a1
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: srli a0, a0, 8
; RV64ZB-NEXT: ret
%m = and i32 %x, 16711680
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -134,17 +119,12 @@ define i32 @bswap_src_and_byte2_i32(i32 %x) {
define i32 @bswap_src_and_byte3_i32(i32 %x) {
; RV32ZB-LABEL: bswap_src_and_byte3_i32:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: lui a1, 1044480
-; RV32ZB-NEXT: and a0, a0, a1
-; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: srli a0, a0, 24
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_and_byte3_i32:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: lui a1, 1044480
-; RV64ZB-NEXT: and a0, a0, a1
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: srliw a0, a0, 24
; RV64ZB-NEXT: ret
%m = and i32 %x, 4278190080
%b = call i32 @llvm.bswap.i32(i32 %m)
@@ -154,15 +134,12 @@ define i32 @bswap_src_and_byte3_i32(i32 %x) {
define i32 @bswap_src_zext_i8_to_i32(i8 %x) {
; RV32ZB-LABEL: bswap_src_zext_i8_to_i32:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: zext.b a0, a0
-; RV32ZB-NEXT: rev8 a0, a0
+; RV32ZB-NEXT: slli a0, a0, 24
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_zext_i8_to_i32:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: zext.b a0, a0
-; RV64ZB-NEXT: rev8 a0, a0
-; RV64ZB-NEXT: srli a0, a0, 32
+; RV64ZB-NEXT: slliw a0, a0, 24
; RV64ZB-NEXT: ret
%z = zext i8 %x to i32
%b = call i32 @llvm.bswap.i32(i32 %z)
@@ -172,15 +149,13 @@ define i32 @bswap_src_zext_i8_to_i32(i8 %x) {
define i64 @bswap_src_zext_i8_to_i64(i8 %x) {
; RV32ZB-LABEL: bswap_src_zext_i8_to_i64:
; RV32ZB: # %bb.0:
-; RV32ZB-NEXT: zext.b a0, a0
-; RV32ZB-NEXT: rev8 a1, a0
+; RV32ZB-NEXT: slli a1, a0, 24
; RV32ZB-NEXT: li a0, 0
; RV32ZB-NEXT: ret
;
; RV64ZB-LABEL: bswap_src_zext_i8_to_i64:
; RV64ZB: # %bb.0:
-; RV64ZB-NEXT: zext.b a0, a0
-; RV64ZB-NEXT: rev8 a0, a0
+; RV64ZB-NEXT: slli a0, a0, 56
; RV64ZB-NEXT: ret
%z = zext i8 %x to i64
%b = call i64 @llvm.bswap.i64(i64 %z)
@@ -204,3 +179,29 @@ define i64 @bswap_src_zext_i16_to_i64(i16 %x) {
%b = call i64 @llvm.bswap.i64(i64 %z)
ret i64 %b
}
+
+; Regression: the producer-side combine must not tag the produced shl
+; with nsw. For bswap(and X, 0xFF) on i16 we shift byte 0 into the top
+; byte, which becomes the sign bit. Bit 7 of the input freely flips the
+; result's sign, so nsw would be unsound and make `icmp slt 0` fold to
+; false. Here the comparison must remain: the result depends on bit 7
+; of the input.
+define i1 @bswap_lo_byte_sign_i16(i16 %x) {
+; RV32ZB-LABEL: bswap_lo_byte_sign_i16:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: slli a0, a0, 8
+; RV32ZB-NEXT: sext.h a0, a0
+; RV32ZB-NEXT: srli a0, a0, 31
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_lo_byte_sign_i16:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: slli a0, a0, 8
+; RV64ZB-NEXT: sext.h a0, a0
+; RV64ZB-NEXT: srli a0, a0, 63
+; RV64ZB-NEXT: ret
+ %m = and i16 %x, 255
+ %b = call i16 @llvm.bswap.i16(i16 %m)
+ %neg = icmp slt i16 %b, 0
+ ret i1 %neg
+}
diff --git a/llvm/test/CodeGen/X86/known-pow2.ll b/llvm/test/CodeGen/X86/known-pow2.ll
index 47bf4e348e26a..fc368cb5a54ac 100644
--- a/llvm/test/CodeGen/X86/known-pow2.ll
+++ b/llvm/test/CodeGen/X86/known-pow2.ll
@@ -1161,12 +1161,10 @@ define i1 @pow2_and_i128(i128 %num, i128 %shift) {
define i32 @pow2_bswap(i32 %a0, i32 %a1) {
; CHECK-LABEL: pow2_bswap:
; CHECK: # %bb.0:
-; CHECK-NEXT: # kill: def $edi killed $edi def $rdi
-; CHECK-NEXT: notl %edi
-; CHECK-NEXT: shrl $31, %edi
-; CHECK-NEXT: leal 4(,%rdi,4), %eax
-; CHECK-NEXT: bswapl %eax
-; CHECK-NEXT: decl %eax
+; CHECK-NEXT: movl %edi, %eax
+; CHECK-NEXT: notl %eax
+; CHECK-NEXT: shrl $5, %eax
+; CHECK-NEXT: orl $67108863, %eax # imm = 0x3FFFFFF
; CHECK-NEXT: andl %esi, %eax
; CHECK-NEXT: retq
%cmp = icmp sgt i32 0, %a0
@@ -1213,12 +1211,10 @@ define i32 @pow2_bitreverse(i32 %a0, i32 %a1) {
; CHECK: # %bb.0:
; CHECK-NEXT: # kill: def $edi killed $edi def $rdi
; CHECK-NEXT: notl %edi
-; CHECK-NEXT: shrl $31, %edi
-; CHECK-NEXT: leal 4(,%rdi,4), %eax
-; CHECK-NEXT: bswapl %eax
-; CHECK-NEXT: movl %eax, %ecx
-; CHECK-NEXT: shll $3, %eax
-; CHECK-NEXT: leal (%rax,%rcx,2), %eax
+; CHECK-NEXT: shrl $5, %edi
+; CHECK-NEXT: andl $67108864, %edi # imm = 0x4000000
+; CHECK-NEXT: leal 536870912(,%rdi,8), %eax
+; CHECK-NEXT: leal 134217728(%rax,%rdi,2), %eax
; CHECK-NEXT: andl $805306368, %eax # imm = 0x30000000
; CHECK-NEXT: decl %eax
; CHECK-NEXT: andl %esi, %eax
>From 63ca265489654515446c4d53a9a59e79bf2ee0f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 14:47:48 +0200
Subject: [PATCH 3/9] [DAGCombiner] Use llvm::AbsoluteDifference in
bswap-to-shift combine
Addresses review feedback from @jayfoad: replace the manual ternary
with the existing llvm::AbsoluteDifference helper from
llvm/Support/MathExtras.h. Semantically equivalent, but idiomatic
(already used in Hexagon backend, MSF, XRay) and avoids signed-cast
roundtrips.
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 5c000412117c5..653bba56a27c2 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12145,8 +12145,7 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
unsigned SrcByte = Lo / 8;
unsigned DstByte = BW / 8 - 1 - SrcByte;
unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
- unsigned Amt =
- (DstByte > SrcByte ? DstByte - SrcByte : SrcByte - DstByte) * 8;
+ unsigned Amt = AbsoluteDifference(DstByte, SrcByte) * 8;
SDNodeFlags Flags =
Opc == ISD::SHL ? SDNodeFlags::NoUnsignedWrap : SDNodeFlags::Exact;
return DAG.getNode(Opc, DL, VT, N0, DAG.getShiftAmountConstant(Amt, VT, DL),
>From c40fa456411514d66346e4e61b93595286adde25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 14:59:46 +0200
Subject: [PATCH 4/9] [DAGCombiner] Tighten bswap combine: assert non-zero
operand, fold SrcByte
Review follow-up:
- Known.isZero() on the bswap operand is unreachable (verified across
the full X86+AArch64+RISCV CodeGen lit suite); replace the runtime
Lo < Hi guard with an assert that documents why Lo == Hi and Lo > Hi
can't occur here.
- Sink SrcByte = Lo / 8 into the if init-statement so we don't compute
Lo / 8 twice.
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 653bba56a27c2..5b00f2b5dd791 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12139,10 +12139,10 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
// to the mirror byte. Producer-side dual of the ISD::BSWAP rule in
// TargetLowering::SimplifyDemandedBits; placed here to fire pre-legalize.
KnownBits Known = DAG.computeKnownBits(N0);
+ assert(!Known.isZero() && "known-zero should be folded to 0 earlier");
unsigned Lo = Known.countMinTrailingZeros();
unsigned Hi = BW - Known.countMinLeadingZeros();
- if (Lo < Hi && Lo / 8 == (Hi - 1) / 8) {
- unsigned SrcByte = Lo / 8;
+ if (unsigned SrcByte = Lo / 8; SrcByte == (Hi - 1) / 8) {
unsigned DstByte = BW / 8 - 1 - SrcByte;
unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
unsigned Amt = AbsoluteDifference(DstByte, SrcByte) * 8;
>From 8c88dd07254ba589867eff378d560da5d6eec184 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 15:33:00 +0200
Subject: [PATCH 5/9] [DAGCombiner] Fold bswap(known-zero) to zero in
visitBSWAP
The previous commit asserted that the bswap operand is never known-zero
at this point (on the premise that earlier combines would fold it
away). That assumption is wrong: computeKnownBits can prove a value is
zero through analyses that structural combines don't replicate. Concrete
example:
%m1 = and i32 %x, 0x00FF ; byte 0 possibly nonzero
%m2 = and i32 %y, 0xFF00 ; byte 1 possibly nonzero
%m3 = and i32 %m1, %m2 ; disjoint masks => always 0
%b = call i32 @llvm.bswap.i32(i32 %m3)
computeKnownBits proves %m3 is zero but the DAG combiner doesn't
simplify the nested ands to a constant before visitBSWAP runs, so the
bswap sees a non-constant known-zero operand. Without this fold the
later assert-!isZero crashes; with it the bswap folds cleanly to 0.
Restore the early-exit as a real combine (bswap(0) = 0) and convert
the range check to an assertion on the remaining non-zero path.
Also add a regression test to AArch64/bswap-known-bits.ll and
RISCV/bswap-known-bits.ll.
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 7 +++++-
llvm/test/CodeGen/AArch64/bswap-known-bits.ll | 17 ++++++++++++++
llvm/test/CodeGen/RISCV/bswap-known-bits.ll | 22 +++++++++++++++++++
3 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 5b00f2b5dd791..7662f37f3cc01 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12139,9 +12139,14 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
// to the mirror byte. Producer-side dual of the ISD::BSWAP rule in
// TargetLowering::SimplifyDemandedBits; placed here to fire pre-legalize.
KnownBits Known = DAG.computeKnownBits(N0);
- assert(!Known.isZero() && "known-zero should be folded to 0 earlier");
+ // bswap(0) = 0. Catch cases that computeKnownBits can prove are zero but
+ // that structural combines haven't simplified to a constant yet
+ // (e.g. and of disjoint byte masks).
+ if (Known.isZero())
+ return DAG.getConstant(0, DL, VT);
unsigned Lo = Known.countMinTrailingZeros();
unsigned Hi = BW - Known.countMinLeadingZeros();
+ assert(Lo < Hi && "non-zero value must have a possibly-nonzero bit");
if (unsigned SrcByte = Lo / 8; SrcByte == (Hi - 1) / 8) {
unsigned DstByte = BW / 8 - 1 - SrcByte;
unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
diff --git a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
index 417db22639ec4..5ae06203e9b59 100644
--- a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
@@ -243,3 +243,20 @@ define i1 @bswap_lo_byte_sign_i16(i16 %x) {
%neg = icmp slt i16 %b, 0
ret i1 %neg
}
+
+; Regression test for the known-zero fold: two disjoint byte masks ANDed
+; together are always zero, but the DAG combiner doesn't structurally
+; simplify to a constant before visiting the bswap. computeKnownBits
+; correctly proves the operand is zero, and the combine must fold
+; bswap(known-zero) to 0.
+define i32 @bswap_nested_and_disjoint_i32(i32 %x, i32 %y) {
+; CHECK-LABEL: bswap_nested_and_disjoint_i32:
+; CHECK: ; %bb.0:
+; CHECK-NEXT: mov w0, wzr
+; CHECK-NEXT: ret
+ %m1 = and i32 %x, 255
+ %m2 = and i32 %y, 65280
+ %m3 = and i32 %m1, %m2
+ %b = call i32 @llvm.bswap.i32(i32 %m3)
+ ret i32 %b
+}
diff --git a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
index 2e2779938d36b..57c56b3dd45eb 100644
--- a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
@@ -205,3 +205,25 @@ define i1 @bswap_lo_byte_sign_i16(i16 %x) {
%neg = icmp slt i16 %b, 0
ret i1 %neg
}
+
+; Regression test for the known-zero fold: two disjoint byte masks ANDed
+; together are always zero, but the DAG combiner doesn't structurally
+; simplify to a constant before visiting the bswap. computeKnownBits
+; correctly proves the operand is zero, and the combine must fold
+; bswap(known-zero) to 0.
+define i32 @bswap_nested_and_disjoint_i32(i32 %x, i32 %y) {
+; RV32ZB-LABEL: bswap_nested_and_disjoint_i32:
+; RV32ZB: # %bb.0:
+; RV32ZB-NEXT: li a0, 0
+; RV32ZB-NEXT: ret
+;
+; RV64ZB-LABEL: bswap_nested_and_disjoint_i32:
+; RV64ZB: # %bb.0:
+; RV64ZB-NEXT: li a0, 0
+; RV64ZB-NEXT: ret
+ %m1 = and i32 %x, 255
+ %m2 = and i32 %y, 65280
+ %m3 = and i32 %m1, %m2
+ %b = call i32 @llvm.bswap.i32(i32 %m3)
+ ret i32 %b
+}
>From d2c0e4b90898fd9d5edd435c5a2cfe7cef5bb83b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 15:40:36 +0200
Subject: [PATCH 6/9] [DAGCombiner][test] Rewrite header comment to describe
the tested invariant
Review feedback from @davemgreen: the header comment framed the tests
as "missed optimization" with "today these emit X; after the fold lands
they should become Y". Once the combine lands that framing goes stale.
Rewrite to describe the test invariant directly.
---
llvm/test/CodeGen/AArch64/bswap-known-bits.ll | 8 +++-----
llvm/test/CodeGen/RISCV/bswap-known-bits.ll | 8 +++-----
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
index 5ae06203e9b59..7e881198ced02 100644
--- a/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/AArch64/bswap-known-bits.ll
@@ -113,11 +113,9 @@ define void @demand_one_loaded_byte(ptr %xp, ptr %yp) {
ret void
}
-; Source-side missed optimizations: when the bswap *operand* is known to have
-; at most one byte of possibly-nonzero bits, the bswap is equivalent to a
-; shift moving that byte to the mirror position. Today these emit
-; rev + shift + mask; after the generic DAGCombiner fold lands, they should
-; become a single shift (or a no-op for i64 middle bytes that don't exist).
+; Source-side known-bits fold: when the bswap operand has at most one byte
+; of possibly-nonzero bits at a known byte-aligned position, the bswap is
+; equivalent to a shift moving that byte to the mirror byte.
define i16 @bswap_src_and_lo_i16(i16 %x) {
; CHECK-LABEL: bswap_src_and_lo_i16:
diff --git a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
index 57c56b3dd45eb..62d332306395b 100644
--- a/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
+++ b/llvm/test/CodeGen/RISCV/bswap-known-bits.ll
@@ -4,11 +4,9 @@
; RUN: llc -mtriple=riscv64 -mattr=+zbb -verify-machineinstrs < %s \
; RUN: | FileCheck %s -check-prefixes=RV64ZB
-; Source-side missed optimizations: when the bswap *operand* is known to have
-; at most one byte of possibly-nonzero bits, the bswap is equivalent to a
-; shift moving that byte to the mirror position. Today these emit
-; mask + rev8 + shift; after the generic DAGCombiner fold lands, they should
-; become a single shift.
+; Source-side known-bits fold: when the bswap operand has at most one byte
+; of possibly-nonzero bits at a known byte-aligned position, the bswap is
+; equivalent to a shift moving that byte to the mirror byte.
declare i16 @llvm.bswap.i16(i16)
declare i32 @llvm.bswap.i32(i32)
>From 850cf4a6afe35a961927983fa3baf47f898e9639 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 17:13:55 +0200
Subject: [PATCH 7/9] [DAGCombiner] Gate bswap-known-byte-to-shift on operation
legality
Targets such as MSP430 custom-lower shl i16 by >= 8 bits into
bswap(zext.i8(x)) + remaining shift. The new bswap-known-byte-to-shift
combine then rewrites that bswap back to shl i16 X, 8, which the
post-legalize combine pass re-legalizes, re-triggering the target's
bswap-based lowering. Result: infinite loop.
Gate the combine on !LegalOperations || hasOperation(Opc, VT) so it
still fires freely pre-legalize (including on illegal types like i256,
which are then legalized together with the produced shift), but post-
legalize only runs on shift opcodes the target supports natively.
Fixes the hang seen on MSP430 CodeGen tests (shifts.ll,
shift-amount-threshold.ll).
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 27 ++++++++++++-------
1 file changed, 17 insertions(+), 10 deletions(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 7662f37f3cc01..9c26a79401d73 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12147,17 +12147,24 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
unsigned Lo = Known.countMinTrailingZeros();
unsigned Hi = BW - Known.countMinLeadingZeros();
assert(Lo < Hi && "non-zero value must have a possibly-nonzero bit");
- if (unsigned SrcByte = Lo / 8; SrcByte == (Hi - 1) / 8) {
- unsigned DstByte = BW / 8 - 1 - SrcByte;
- unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
- unsigned Amt = AbsoluteDifference(DstByte, SrcByte) * 8;
- SDNodeFlags Flags =
- Opc == ISD::SHL ? SDNodeFlags::NoUnsignedWrap : SDNodeFlags::Exact;
- return DAG.getNode(Opc, DL, VT, N0, DAG.getShiftAmountConstant(Amt, VT, DL),
- Flags);
- }
+ unsigned SrcByte = Lo / 8;
+ if (SrcByte != (Hi - 1) / 8)
+ return SDValue();
- return SDValue();
+ unsigned DstByte = BW / 8 - 1 - SrcByte;
+ unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
+ // Skip if the target would re-expand the produced shift post-legalize.
+ // Targets that custom-lower byte-multiple shifts via bswap (e.g. MSP430
+ // for shl i16) would loop with this combine. Same idiom as the
+ // hasOperation check on the bswap-shl-half combine above.
+ if (LegalOperations && !hasOperation(Opc, VT))
+ return SDValue();
+
+ unsigned Amt = AbsoluteDifference(DstByte, SrcByte) * 8;
+ SDNodeFlags Flags =
+ Opc == ISD::SHL ? SDNodeFlags::NoUnsignedWrap : SDNodeFlags::Exact;
+ return DAG.getNode(Opc, DL, VT, N0, DAG.getShiftAmountConstant(Amt, VT, DL),
+ Flags);
}
SDValue DAGCombiner::visitBITREVERSE(SDNode *N) {
>From 61d8e492463b60dd349400fea9f47c7e5cd02b56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Wed, 22 Apr 2026 22:23:32 +0200
Subject: [PATCH 8/9] [DAGCombiner] Restructure bswap KnownBits folds for
extensibility
Per review: the tail of visitBSWAP used cascading early returns, so
adding a new combine after the known-byte-to-shift fold required
unwinding them. Restore the original single-if-block shape: return
only on a positive match, and fall through to the final
`return SDValue();`. No functional change.
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 41 +++++++++----------
1 file changed, 20 insertions(+), 21 deletions(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 9c26a79401d73..8470346e59696 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12135,36 +12135,35 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
if (SDValue V = foldBitOrderCrossLogicOp(N, DAG))
return V;
- // If only one byte of the operand may be nonzero, bswap becomes a shift
- // to the mirror byte. Producer-side dual of the ISD::BSWAP rule in
- // TargetLowering::SimplifyDemandedBits; placed here to fire pre-legalize.
+ // Folds that depend on computeKnownBits of the operand. Producer-side dual
+ // of the ISD::BSWAP rule in TargetLowering::SimplifyDemandedBits; placed
+ // here to fire pre-legalize.
KnownBits Known = DAG.computeKnownBits(N0);
// bswap(0) = 0. Catch cases that computeKnownBits can prove are zero but
// that structural combines haven't simplified to a constant yet
// (e.g. and of disjoint byte masks).
if (Known.isZero())
return DAG.getConstant(0, DL, VT);
+ // If only one byte of the operand may be nonzero, bswap becomes a shift
+ // to the mirror byte.
unsigned Lo = Known.countMinTrailingZeros();
unsigned Hi = BW - Known.countMinLeadingZeros();
- assert(Lo < Hi && "non-zero value must have a possibly-nonzero bit");
- unsigned SrcByte = Lo / 8;
- if (SrcByte != (Hi - 1) / 8)
- return SDValue();
-
- unsigned DstByte = BW / 8 - 1 - SrcByte;
- unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
- // Skip if the target would re-expand the produced shift post-legalize.
- // Targets that custom-lower byte-multiple shifts via bswap (e.g. MSP430
- // for shl i16) would loop with this combine. Same idiom as the
- // hasOperation check on the bswap-shl-half combine above.
- if (LegalOperations && !hasOperation(Opc, VT))
- return SDValue();
+ if (unsigned SrcByte = Lo / 8; SrcByte == (Hi - 1) / 8) {
+ unsigned DstByte = BW / 8 - 1 - SrcByte;
+ unsigned Opc = DstByte > SrcByte ? ISD::SHL : ISD::SRL;
+ // Skip if the target would re-expand the produced shift post-legalize.
+ // Targets that custom-lower byte-multiple shifts via bswap (e.g. MSP430
+ // for shl i16) would loop with this combine.
+ if (!LegalOperations || hasOperation(Opc, VT)) {
+ unsigned Amt = AbsoluteDifference(DstByte, SrcByte) * 8;
+ SDNodeFlags Flags =
+ Opc == ISD::SHL ? SDNodeFlags::NoUnsignedWrap : SDNodeFlags::Exact;
+ return DAG.getNode(Opc, DL, VT, N0,
+ DAG.getShiftAmountConstant(Amt, VT, DL), Flags);
+ }
+ }
- unsigned Amt = AbsoluteDifference(DstByte, SrcByte) * 8;
- SDNodeFlags Flags =
- Opc == ISD::SHL ? SDNodeFlags::NoUnsignedWrap : SDNodeFlags::Exact;
- return DAG.getNode(Opc, DL, VT, N0, DAG.getShiftAmountConstant(Amt, VT, DL),
- Flags);
+ return SDValue();
}
SDValue DAGCombiner::visitBITREVERSE(SDNode *N) {
>From 71a26990e0c3974ae469e2a4dc906476c0fdd04c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bylica?= <pawel at hepcolgum.band>
Date: Thu, 23 Apr 2026 10:45:29 +0200
Subject: [PATCH 9/9] [DAGCombiner] Address bswap review comments
- Remove redundant `BW % 16 == 0` assert in visitBSWAP: SelectionDAG::getNode
already enforces this when the BSWAP node is created.
- Drop the cross-reference to `TargetLowering::SimplifyDemandedBits` from
the KnownBits-folds comment; naming another rule by its function location
is the kind of pointer that rots over time.
---
llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 8470346e59696..3770d48049383 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -12095,8 +12095,6 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
}
unsigned BW = VT.getScalarSizeInBits();
- assert(BW % 16 == 0 && "bswap requires a multiple-of-16-bit width");
-
// fold (bswap shl(x,c)) -> (zext(bswap(trunc(shl(x,sub(c,bw/2))))))
// iff x >= bw/2 (i.e. lower half is known zero)
if (BW >= 32 && N0.getOpcode() == ISD::SHL && N0.hasOneUse()) {
@@ -12135,9 +12133,7 @@ SDValue DAGCombiner::visitBSWAP(SDNode *N) {
if (SDValue V = foldBitOrderCrossLogicOp(N, DAG))
return V;
- // Folds that depend on computeKnownBits of the operand. Producer-side dual
- // of the ISD::BSWAP rule in TargetLowering::SimplifyDemandedBits; placed
- // here to fire pre-legalize.
+ // Folds that depend on computeKnownBits of the operand.
KnownBits Known = DAG.computeKnownBits(N0);
// bswap(0) = 0. Catch cases that computeKnownBits can prove are zero but
// that structural combines haven't simplified to a constant yet
More information about the llvm-commits
mailing list