Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cf2f9db
[InstCombine] Add pre-commit tests for boolean canonicalization (NFC)
yafet-a Jul 19, 2025
3a55b19
[InstCombine] Optimised expressions in issue #97044
yafet-a Jul 21, 2025
02807e3
3 input handled via truth table
yafet-a Jul 29, 2025
af90743
Merge branch 'main' into users/yafet-a/boolean-optimisation
yafet-a Jul 29, 2025
d066a85
Move simple expression check to caller
yafet-a Aug 7, 2025
4c86e54
removed recursion + smallptrset used
yafet-a Aug 7, 2025
7a2dc67
moved calls to consistent location in each visit function + added cal…
yafet-a Aug 7, 2025
6772db5
traverse only if node belongs to expr tree
yafet-a Aug 11, 2025
5405486
Refactor
yafet-a Aug 11, 2025
cb1e164
Merge branch 'main' into users/yafet-a/boolean-optimisation
yafet-a Aug 11, 2025
650a7ab
check for instructions being in the same bb to avoid comesBefore cros…
yafet-a Aug 13, 2025
18e576e
review (batch evaluation, refactors)
yafet-a Aug 14, 2025
28d4a0f
Add negative tests
yafet-a Aug 14, 2025
1fee55f
correctly checking for vars in same bb in extractThreeVariables()
yafet-a Aug 14, 2025
a39a3b4
reuse visited set in extractThreeVariables
yafet-a Aug 15, 2025
23feb15
multi-use tests + negative tests with and/or Var, Const nodes
yafet-a Aug 15, 2025
1a94bba
Computed Map validation in extractThreeVar
yafet-a Aug 15, 2025
d19190d
Pass instructions by reference instead of returning vectors
yafet-a Aug 19, 2025
9296d9b
early check for invalid num of variables
yafet-a Aug 20, 2025
48bd1ca
Improved sorting
yafet-a Aug 20, 2025
464d95e
treat non-bitwise ops as leaf nodes with use-count heuristic
yafet-a Aug 20, 2025
2a905fb
format
yafet-a Aug 20, 2025
fc2aac4
format-2
yafet-a Aug 20, 2025
d557827
validate no cross-BB instruction order comparison for computation ins…
yafet-a Aug 26, 2025
5c40046
(NFC: Styling) + Structural Similarity Check for loop
yafet-a Aug 29, 2025
7bf8caf
Traverse root operands to avoid treating them as leaf variables
yafet-a Aug 29, 2025
abd628d
NFC: negative test for treating root operands as leaf variables
yafet-a Aug 29, 2025
64fbafd
style: header comments
yafet-a Sep 1, 2025
ecd9669
[Tests] vector tests
yafet-a Sep 1, 2025
2b18e01
[NIT] improving header comments
yafet-a Sep 1, 2025
b238960
Merge branch 'main' into users/yafet-a/boolean-optimisation
ElvinaYakubova Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "llvm/IR/PatternMatch.h"
#include "llvm/Transforms/InstCombine/InstCombiner.h"
#include "llvm/Transforms/Utils/Local.h"
#include <bitset>

using namespace llvm;
using namespace PatternMatch;
Expand Down Expand Up @@ -50,6 +51,205 @@ static Value *getFCmpValue(unsigned Code, Value *LHS, Value *RHS,
return Builder.CreateFCmpFMF(NewPred, LHS, RHS, FMF);
}

/// This is to create optimal 3-variable boolean logic from truth tables.
/// currently it supports the cases pertaining to the issue 97044. More cases
/// can be added based on real-world justification for specific 3 input cases
/// or with reviewer approval all 256 cases can be added (choose the
/// canonicalizations found
/// in x86InstCombine.cpp?)
static Value *createLogicFromTable3Var(const std::bitset<8> &Table, Value *Op0,
Value *Op1, Value *Op2, Value *Root,
IRBuilderBase &Builder) {
uint8_t TruthValue = Table.to_ulong();
auto FoldConstant = [&](bool Val) {
Type *Ty = Op0->getType();
return Val ? ConstantInt::getTrue(Ty) : ConstantInt::getFalse(Ty);
};

Value *Result = nullptr;
switch (TruthValue) {
default:
return nullptr;
case 0x00: // Always FALSE
Result = FoldConstant(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Result = FoldConstant(false);
Result = ConstantInt::getFalse(Op0->getType());

I don't think we need a helper function for this... (also for true below).

break;
case 0xFF: // Always TRUE
Result = FoldConstant(true);
break;
case 0xE1: // ~((Op1 | Op2) ^ Op0)
{
Value *Or = Builder.CreateOr(Op1, Op2);
Value *Xor = Builder.CreateXor(Or, Op0);
Result = Builder.CreateNot(Xor);
} break;
case 0x60: // Op0 & (Op1 ^ Op2)
{
Value *Xor = Builder.CreateXor(Op1, Op2);
Result = Builder.CreateAnd(Op0, Xor);
} break;
case 0xD2: // ((Op1 | Op2) ^ Op0) ^ Op1
{
Value *Or = Builder.CreateOr(Op1, Op2);
Value *Xor1 = Builder.CreateXor(Or, Op0);
Result = Builder.CreateXor(Xor1, Op1);
} break;
}

return Result;
}

static std::tuple<Value *, Value *, Value *>
extractThreeVariables(Value *Root) {
SmallPtrSet<Value *, 3> Variables;
SmallPtrSet<Value *, 32> Visited; // Prevent hanging during loop unrolling
// (see bitreverse-hang.ll)
SmallVector<Value *> Worklist;
Worklist.push_back(Root);

// Track all instructions to ensure they're in the same BB
BasicBlock *FirstBB = nullptr;

while (!Worklist.empty()) {
Value *V = Worklist.pop_back_val();

Value *NotV;
if (match(V, m_Not(m_Value(NotV)))) {
Visited.insert(NotV);
if (V == Root ||
V->hasOneUse()) { // Due to lack of cost-based heuristic, only
// traverse if it belongs to this expression tree
Worklist.push_back(NotV);
}
continue;
}
if (auto *BO = dyn_cast<BinaryOperator>(V)) {
if (!BO->isBitwiseLogicOp())
return {nullptr, nullptr, nullptr};

// Check BB consistency
if (!FirstBB)
FirstBB = BO->getParent();
else if (BO->getParent() != FirstBB)
return {nullptr, nullptr, nullptr};

if (V == Root || V->hasOneUse()) {
Visited.insert(BO->getOperand(0));
Visited.insert(BO->getOperand(1));
Worklist.push_back(BO->getOperand(0));
Worklist.push_back(BO->getOperand(1));
}
} else if (isa<Argument>(V) || isa<Instruction>(V)) {
if (!isa<Constant>(V) && V != Root) {
Variables.insert(V);
}
}
}

if (Variables.size() == 3) {
// Sort variables by instruction order
SmallVector<Value *, 3> SortedVars(Variables.begin(), Variables.end());
llvm::sort(SortedVars, [](Value *A, Value *B) {
if (auto *IA = dyn_cast<Instruction>(A))
if (auto *IB = dyn_cast<Instruction>(B))
return IA->comesBefore(IB);
return A < B;
});
return {SortedVars[0], SortedVars[1], SortedVars[2]};
}
return {nullptr, nullptr, nullptr};
}

/// Evaluate a boolean expression with bit-vector inputs for all 8 combinations.
static std::optional<std::bitset<8>>
evaluateBooleanExpression(Value *Expr, Value *Op0, Value *Op1, Value *Op2) {
// Post-order traversal of the expression tree
SmallVector<Instruction *> Instructions;
SmallVector<Value *> ToVisit;
SmallPtrSet<Instruction *, 8> Seen;

ToVisit.push_back(Expr);
while (!ToVisit.empty()) {
Value *V = ToVisit.pop_back_val();
if (auto *I = dyn_cast<Instruction>(V)) {
if (Seen.insert(I).second) {
Instructions.push_back(I);
for (Value *Op : I->operands()) {
ToVisit.push_back(Op);
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that what this actually does is discarding cases with constant operands, as everything else should have already been handled in the initial loop? If so, can we bail out on constant operands there already?


// Sort instructions within the same BB
llvm::sort(Instructions,
[](Instruction *A, Instruction *B) { return A->comesBefore(B); });

// Initialize bit-vector values for the 3 variables
// Op0: 0b11110000 (true for combinations 000,001,010,011)
// Op1: 0b11001100 (true for combinations 000,001,100,101)
// Op2: 0b10101010 (true for combinations 000,010,100,110)
SmallDenseMap<Value *, std::bitset<8>> Computed;
Computed[Op0] = std::bitset<8>(0xF0); // 11110000
Computed[Op1] = std::bitset<8>(0xCC); // 11001100
Computed[Op2] = std::bitset<8>(0xAA); // 10101010

for (Instruction *I : Instructions) {
Value *NotV;
if (match(I, m_Not(m_Value(NotV)))) {
if (!Computed.count(NotV))
return std::nullopt;
Computed[I] = ~Computed.at(NotV); // Bitwise NOT
} else if (auto *BO = dyn_cast<BinaryOperator>(I)) {
if (!Computed.count(BO->getOperand(0)) ||
!Computed.count(BO->getOperand(1)))
return std::nullopt;

auto &LHS = Computed.at(BO->getOperand(0));
auto &RHS = Computed.at(BO->getOperand(1));

switch (BO->getOpcode()) {
case Instruction::And:
Computed[I] = LHS & RHS; // Bitwise AND
break;
case Instruction::Or:
Computed[I] = LHS | RHS; // Bitwise OR
break;
case Instruction::Xor:
Computed[I] = LHS ^ RHS; // Bitwise XOR
break;
default:
return std::nullopt;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the case where it's neither Not nor BinaryOperator? I assume it can't happen, in which case we should make that dyn_cast above a cast.

}

auto It = Computed.find(Expr);
return It != Computed.end() ? std::optional<std::bitset<8>>(It->second)
: std::nullopt;
}

/// Try to canonicalize 3-variable boolean expressions using truth table lookup.
static Value *foldThreeVarBoolExpr(Instruction &Root,
InstCombiner::BuilderTy &Builder) {

auto &BO = cast<BinaryOperator>(Root);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly accept BinaryOperator as the argument? (It will downcast to Instruction where needed.)

assert(BO.isBitwiseLogicOp() && "Unexpected opcode for boolean expression");

if (!isa<BinaryOperator>(BO.getOperand(0)) ||
!isa<BinaryOperator>(BO.getOperand(1)))
return nullptr;

auto [Op0, Op1, Op2] = extractThreeVariables(&Root);
if (!Op0 || !Op1 || !Op2)
return nullptr;

auto Table = evaluateBooleanExpression(&Root, Op0, Op1, Op2);
if (!Table)
return nullptr;

return createLogicFromTable3Var(*Table, Op0, Op1, Op2, &Root, Builder);
}

/// Emit a computation of: (V >= Lo && V < Hi) if Inside is true, otherwise
/// (V < Lo || V >= Hi). This method expects that Lo < Hi. IsSigned indicates
/// whether to treat V, Lo, and Hi as signed or not.
Expand Down Expand Up @@ -2400,6 +2600,9 @@ Instruction *InstCombinerImpl::visitAnd(BinaryOperator &I) {
if (Instruction *Phi = foldBinopWithPhiOperands(I))
return Phi;

if (Value *Canonical = foldThreeVarBoolExpr(I, Builder))
return replaceInstUsesWith(I, Canonical);

// See if we can simplify any instructions used by the instruction whose sole
// purpose is to compute bits we don't care about.
if (SimplifyDemandedInstructionBits(I))
Expand Down Expand Up @@ -3905,6 +4108,9 @@ Instruction *InstCombinerImpl::visitOr(BinaryOperator &I) {
if (Instruction *Phi = foldBinopWithPhiOperands(I))
return Phi;

if (Value *Canonical = foldThreeVarBoolExpr(I, Builder))
return replaceInstUsesWith(I, Canonical);

// See if we can simplify any instructions used by the instruction whose sole
// purpose is to compute bits we don't care about.
if (SimplifyDemandedInstructionBits(I))
Expand Down Expand Up @@ -5055,6 +5261,9 @@ Instruction *InstCombinerImpl::visitXor(BinaryOperator &I) {
if (Instruction *Phi = foldBinopWithPhiOperands(I))
return Phi;

if (Value *Canonical = foldThreeVarBoolExpr(I, Builder))
return replaceInstUsesWith(I, Canonical);

if (Instruction *NewXor = foldXorToXor(I, Builder))
return NewXor;

Expand Down
154 changes: 154 additions & 0 deletions llvm/test/Transforms/InstCombine/pr97044.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
; RUN: opt < %s -passes=instcombine -S | FileCheck %s
; Tests for GitHub issue #97044 - Boolean expression canonicalization
define i32 @test0_4way_or(i32 %x, i32 %y, i32 %z) {
; CHECK-LABEL: @test0_4way_or(
; CHECK-NEXT: [[TMP1:%.*]] = or i32 [[Y:%.*]], [[Z:%.*]]
; CHECK-NEXT: [[TMP2:%.*]] = xor i32 [[TMP1]], [[X:%.*]]
; CHECK-NEXT: [[OR13:%.*]] = xor i32 [[TMP2]], -1
; CHECK-NEXT: ret i32 [[OR13]]
;
%not = xor i32 %z, -1
%and = and i32 %y, %not
%and1 = and i32 %and, %x
%not2 = xor i32 %y, -1
%and3 = and i32 %x, %not2
%and4 = and i32 %and3, %z
%or = or i32 %and1, %and4
%not5 = xor i32 %x, -1
%not6 = xor i32 %y, -1
%and7 = and i32 %not5, %not6
%not8 = xor i32 %z, -1
%and9 = and i32 %and7, %not8
%or10 = or i32 %or, %and9
%and11 = and i32 %x, %y
%and12 = and i32 %and11, %z
%or13 = or i32 %or10, %and12
ret i32 %or13
}
define i32 @test1_xor_pattern(i32 %x, i32 %y, i32 %z) {
; CHECK-LABEL: @test1_xor_pattern(
; CHECK-NEXT: [[TMP1:%.*]] = or i32 [[Y:%.*]], [[Z:%.*]]
; CHECK-NEXT: [[TMP2:%.*]] = xor i32 [[TMP1]], [[X:%.*]]
; CHECK-NEXT: [[XOR:%.*]] = xor i32 [[TMP2]], -1
; CHECK-NEXT: ret i32 [[XOR]]
;
%not = xor i32 %z, -1
%and = and i32 %x, %y
%not1 = xor i32 %x, -1
%not2 = xor i32 %y, -1
%and3 = and i32 %not1, %not2
%or = or i32 %and, %and3
%and4 = and i32 %not, %or
%and5 = and i32 %x, %y
%and6 = and i32 %x, %not2
%or7 = or i32 %and5, %and6
%and8 = and i32 %z, %or7
%xor = xor i32 %and4, %and8
ret i32 %xor
}
define i32 @test2_nested_xor(i32 %x, i32 %y, i32 %z) {
; CHECK-LABEL: @test2_nested_xor(
; CHECK-NEXT: [[TMP1:%.*]] = or i32 [[Y:%.*]], [[Z:%.*]]
; CHECK-NEXT: [[TMP2:%.*]] = xor i32 [[TMP1]], [[X:%.*]]
; CHECK-NEXT: [[TMP3:%.*]] = xor i32 [[TMP2]], [[Y]]
; CHECK-NEXT: ret i32 [[TMP3]]
;
%and = and i32 %x, %y
%not = xor i32 %x, -1
%not1 = xor i32 %y, -1
%and2 = and i32 %not, %not1
%or = or i32 %and, %and2
%and3 = and i32 %x, %y
%not4 = xor i32 %y, -1
%and5 = and i32 %x, %not4
%or6 = or i32 %and3, %and5
%xor = xor i32 %or, %or6
%not7 = xor i32 %y, -1
%and8 = and i32 %z, %not7
%and9 = and i32 %xor, %and8
%xor10 = xor i32 %or, %and9
%xor11 = xor i32 %xor10, %y
%xor12 = xor i32 %xor11, -1
ret i32 %xor12
}
define i32 @test3_already_optimal(i32 %x, i32 %y, i32 %z) {
; CHECK-LABEL: @test3_already_optimal(
; CHECK-NEXT: [[OR:%.*]] = or i32 [[Y:%.*]], [[Z:%.*]]
; CHECK-NEXT: [[XOR:%.*]] = xor i32 [[OR]], [[X:%.*]]
; CHECK-NEXT: [[NOT:%.*]] = xor i32 [[XOR]], -1
; CHECK-NEXT: ret i32 [[NOT]]
;
%or = or i32 %y, %z
%xor = xor i32 %or, %x
%not = xor i32 %xor, -1
ret i32 %not
}
; Negative Tests
; Test with non-bitwise operation (should not transform - add/sub not supported)
define i32 @negative_non_bitwise_add(i32 %x, i32 %y, i32 %z) {
; CHECK-LABEL: @negative_non_bitwise_add(
; CHECK-NEXT: [[ADD1:%.*]] = add i32 [[X:%.*]], [[Y:%.*]]
; CHECK-NEXT: [[ADD2:%.*]] = add i32 [[ADD1]], [[Z:%.*]]
; CHECK-NEXT: ret i32 [[ADD2]]
;
%add1 = add i32 %x, %y
%add2 = add i32 %add1, %z
ret i32 %add2
}
; Test with only 2 variables (should not transform - needs exactly 3 variables)
define i32 @negative_two_variables(i32 %x, i32 %y) {
; CHECK-LABEL: @negative_two_variables(
; CHECK-NEXT: [[AND:%.*]] = and i32 [[X:%.*]], [[Y:%.*]]
; CHECK-NEXT: [[NOT:%.*]] = xor i32 [[AND]], -1
; CHECK-NEXT: ret i32 [[NOT]]
;
%and = and i32 %x, %y
%not = xor i32 %and, -1
ret i32 %not
}
; Test with 4 variables (should not transform - needs exactly 3 variables)
define i32 @negative_four_variables(i32 %x, i32 %y, i32 %z, i32 %w) {
; CHECK-LABEL: @negative_four_variables(
; CHECK-NEXT: [[AND1:%.*]] = and i32 [[X:%.*]], [[Y:%.*]]
; CHECK-NEXT: [[AND2:%.*]] = and i32 [[Z:%.*]], [[W:%.*]]
; CHECK-NEXT: [[OR:%.*]] = or i32 [[AND1]], [[AND2]]
; CHECK-NEXT: ret i32 [[OR]]
;
%and1 = and i32 %x, %y
%and2 = and i32 %z, %w
%or = or i32 %and1, %and2
ret i32 %or
}
; Test with simple 2-level expression (should not transform - not complex enough)
define i32 @negative_simple_expression(i32 %x, i32 %y, i32 %z) {
; CHECK-LABEL: @negative_simple_expression(
; CHECK-NEXT: [[AND:%.*]] = and i32 [[X:%.*]], [[Y:%.*]]
; CHECK-NEXT: [[OR:%.*]] = or i32 [[AND]], [[Z:%.*]]
; CHECK-NEXT: ret i32 [[OR]]
;
%and = and i32 %x, %y
%or = or i32 %and, %z
ret i32 %or
}
; Test with instructions in different basic blocks (should not transform)
define i32 @negative_different_basic_blocks(i32 %x, i32 %y, i32 %z, i1 %cond) {
; CHECK-LABEL: @negative_different_basic_blocks(
; CHECK-NEXT: entry:
; CHECK-NEXT: [[AND1:%.*]] = and i32 [[X:%.*]], [[Y:%.*]]
; CHECK-NEXT: br i1 [[COND:%.*]], label [[IF_TRUE:%.*]], label [[IF_FALSE:%.*]]
; CHECK: if.true:
; CHECK-NEXT: [[AND2:%.*]] = and i32 [[AND1]], [[Z:%.*]]
; CHECK-NEXT: ret i32 [[AND2]]
; CHECK: if.false:
; CHECK-NEXT: ret i32 [[AND1]]
;
entry:
%and1 = and i32 %x, %y
br i1 %cond, label %if.true, label %if.false
if.true:
%and2 = and i32 %and1, %z
ret i32 %and2
if.false:
ret i32 %and1
}
Loading