llvm-project/llvm/unittests/Analysis/SparsePropagation.cpp

544 lines
21 KiB
C++
Raw Normal View History

//===- SparsePropagation.cpp - Unit tests for the generic solver ----------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/Analysis/SparsePropagation.h"
#include "llvm/ADT/PointerIntPair.h"
#include "llvm/IR/CallSite.h"
#include "llvm/IR/IRBuilder.h"
#include "gtest/gtest.h"
using namespace llvm;
namespace {
/// To enable interprocedural analysis, we assign LLVM values to the following
/// groups. The register group represents SSA registers, the return group
/// represents the return values of functions, and the memory group represents
/// in-memory values. An LLVM Value can technically be in more than one group.
/// It's necessary to distinguish these groups so we can, for example, track a
/// global variable separately from the value stored at its location.
enum class IPOGrouping { Register, Return, Memory };
/// Our LatticeKeys are PointerIntPairs composed of LLVM values and groupings.
/// The PointerIntPair header provides a DenseMapInfo specialization, so using
/// these as LatticeKeys is fine.
using TestLatticeKey = PointerIntPair<Value *, 2, IPOGrouping>;
} // namespace
namespace llvm {
/// A specialization of LatticeKeyInfo for TestLatticeKeys. The generic solver
/// must translate between LatticeKeys and LLVM Values when adding Values to
/// its work list and inspecting the state of control-flow related values.
template <> struct LatticeKeyInfo<TestLatticeKey> {
static inline Value *getValueFromLatticeKey(TestLatticeKey Key) {
return Key.getPointer();
}
static inline TestLatticeKey getLatticeKeyFromValue(Value *V) {
return TestLatticeKey(V, IPOGrouping::Register);
}
};
} // namespace llvm
namespace {
/// This class defines a simple test lattice value that could be used for
/// solving problems similar to constant propagation. The value is maintained
/// as a PointerIntPair.
class TestLatticeVal {
public:
/// The states of the lattices value. Only the ConstantVal state is
/// interesting; the rest are special states used by the generic solver. The
/// UntrackedVal state differs from the other three in that the generic
/// solver uses it to avoid doing unnecessary work. In particular, when a
/// value moves to the UntrackedVal state, it's users are not notified.
enum TestLatticeStateTy {
UndefinedVal,
ConstantVal,
OverdefinedVal,
UntrackedVal
};
TestLatticeVal() : LatticeVal(nullptr, UndefinedVal) {}
TestLatticeVal(Constant *C, TestLatticeStateTy State)
: LatticeVal(C, State) {}
/// Return true if this lattice value is in the Constant state. This is used
/// for checking the solver results.
bool isConstant() const { return LatticeVal.getInt() == ConstantVal; }
/// Return true if this lattice value is in the Overdefined state. This is
/// used for checking the solver results.
bool isOverdefined() const { return LatticeVal.getInt() == OverdefinedVal; }
bool operator==(const TestLatticeVal &RHS) const {
return LatticeVal == RHS.LatticeVal;
}
bool operator!=(const TestLatticeVal &RHS) const {
return LatticeVal != RHS.LatticeVal;
}
private:
/// A simple lattice value type for problems similar to constant propagation.
/// It holds the constant value and the lattice state.
PointerIntPair<const Constant *, 2, TestLatticeStateTy> LatticeVal;
};
/// This class defines a simple test lattice function that could be used for
/// solving problems similar to constant propagation. The test lattice differs
/// from a "real" lattice in a few ways. First, it initializes all return
/// values, values stored in global variables, and arguments in the undefined
/// state. This means that there are no limitations on what we can track
/// interprocedurally. For simplicity, all global values in the tests will be
/// given internal linkage, since this is not something this lattice function
/// tracks. Second, it only handles the few instructions necessary for the
/// tests.
class TestLatticeFunc
: public AbstractLatticeFunction<TestLatticeKey, TestLatticeVal> {
public:
/// Construct a new test lattice function with special values for the
/// Undefined, Overdefined, and Untracked states.
TestLatticeFunc()
: AbstractLatticeFunction(
TestLatticeVal(nullptr, TestLatticeVal::UndefinedVal),
TestLatticeVal(nullptr, TestLatticeVal::OverdefinedVal),
TestLatticeVal(nullptr, TestLatticeVal::UntrackedVal)) {}
/// Compute and return a TestLatticeVal for the given TestLatticeKey. For the
/// test analysis, a LatticeKey will begin in the undefined state, unless it
/// represents an LLVM Constant in the register grouping.
TestLatticeVal ComputeLatticeVal(TestLatticeKey Key) override {
if (Key.getInt() == IPOGrouping::Register)
if (auto *C = dyn_cast<Constant>(Key.getPointer()))
return TestLatticeVal(C, TestLatticeVal::ConstantVal);
return getUndefVal();
}
/// Merge the two given lattice values. This merge should be equivalent to
/// what is done for constant propagation. That is, the resulting lattice
/// value is constant only if the two given lattice values are constant and
/// hold the same value.
TestLatticeVal MergeValues(TestLatticeVal X, TestLatticeVal Y) override {
if (X == getUntrackedVal() || Y == getUntrackedVal())
return getUntrackedVal();
if (X == getOverdefinedVal() || Y == getOverdefinedVal())
return getOverdefinedVal();
if (X == getUndefVal() && Y == getUndefVal())
return getUndefVal();
if (X == getUndefVal())
return Y;
if (Y == getUndefVal())
return X;
if (X == Y)
return X;
return getOverdefinedVal();
}
/// Compute the lattice values that change as a result of executing the given
/// instruction. We only handle the few instructions needed for the tests.
void ComputeInstructionState(
Instruction &I, DenseMap<TestLatticeKey, TestLatticeVal> &ChangedValues,
SparseSolver<TestLatticeKey, TestLatticeVal> &SS) override {
switch (I.getOpcode()) {
case Instruction::Call:
return visitCallSite(cast<CallInst>(&I), ChangedValues, SS);
case Instruction::Ret:
return visitReturn(*cast<ReturnInst>(&I), ChangedValues, SS);
case Instruction::Store:
return visitStore(*cast<StoreInst>(&I), ChangedValues, SS);
default:
return visitInst(I, ChangedValues, SS);
}
}
private:
/// Handle call sites. The state of a called function's argument is the merge
/// of the current formal argument state with the call site's corresponding
/// actual argument state. The call site state is the merge of the call site
/// state with the returned value state of the called function.
void visitCallSite(CallSite CS,
DenseMap<TestLatticeKey, TestLatticeVal> &ChangedValues,
SparseSolver<TestLatticeKey, TestLatticeVal> &SS) {
Function *F = CS.getCalledFunction();
Instruction *I = CS.getInstruction();
auto RegI = TestLatticeKey(I, IPOGrouping::Register);
if (!F) {
ChangedValues[RegI] = getOverdefinedVal();
return;
}
SS.MarkBlockExecutable(&F->front());
for (Argument &A : F->args()) {
auto RegFormal = TestLatticeKey(&A, IPOGrouping::Register);
auto RegActual =
TestLatticeKey(CS.getArgument(A.getArgNo()), IPOGrouping::Register);
ChangedValues[RegFormal] =
MergeValues(SS.getValueState(RegFormal), SS.getValueState(RegActual));
}
auto RetF = TestLatticeKey(F, IPOGrouping::Return);
ChangedValues[RegI] =
MergeValues(SS.getValueState(RegI), SS.getValueState(RetF));
}
/// Handle return instructions. The function's return state is the merge of
/// the returned value state and the function's current return state.
void visitReturn(ReturnInst &I,
DenseMap<TestLatticeKey, TestLatticeVal> &ChangedValues,
SparseSolver<TestLatticeKey, TestLatticeVal> &SS) {
Function *F = I.getParent()->getParent();
if (F->getReturnType()->isVoidTy())
return;
auto RegR = TestLatticeKey(I.getReturnValue(), IPOGrouping::Register);
auto RetF = TestLatticeKey(F, IPOGrouping::Return);
ChangedValues[RetF] =
MergeValues(SS.getValueState(RegR), SS.getValueState(RetF));
}
/// Handle store instructions. If the pointer operand of the store is a
/// global variable, we attempt to track the value. The global variable state
/// is the merge of the stored value state with the current global variable
/// state.
void visitStore(StoreInst &I,
DenseMap<TestLatticeKey, TestLatticeVal> &ChangedValues,
SparseSolver<TestLatticeKey, TestLatticeVal> &SS) {
auto *GV = dyn_cast<GlobalVariable>(I.getPointerOperand());
if (!GV)
return;
auto RegVal = TestLatticeKey(I.getValueOperand(), IPOGrouping::Register);
auto MemPtr = TestLatticeKey(GV, IPOGrouping::Memory);
ChangedValues[MemPtr] =
MergeValues(SS.getValueState(RegVal), SS.getValueState(MemPtr));
}
/// Handle all other instructions. All other instructions are marked
/// overdefined.
void visitInst(Instruction &I,
DenseMap<TestLatticeKey, TestLatticeVal> &ChangedValues,
SparseSolver<TestLatticeKey, TestLatticeVal> &SS) {
auto RegI = TestLatticeKey(&I, IPOGrouping::Register);
ChangedValues[RegI] = getOverdefinedVal();
}
};
/// This class defines the common data used for all of the tests. The tests
/// should add code to the module and then run the solver.
class SparsePropagationTest : public testing::Test {
protected:
LLVMContext Context;
Module M;
IRBuilder<> Builder;
TestLatticeFunc Lattice;
SparseSolver<TestLatticeKey, TestLatticeVal> Solver;
public:
SparsePropagationTest()
: M("", Context), Builder(Context), Solver(&Lattice) {}
};
} // namespace
/// Test that we mark discovered functions executable.
///
/// define internal void @f() {
/// call void @g()
/// ret void
/// }
///
/// define internal void @g() {
/// call void @f()
/// ret void
/// }
///
/// For this test, we initially mark "f" executable, and the solver discovers
/// "g" because of the call in "f". The mutually recursive call in "g" also
/// tests that we don't add a block to the basic block work list if it is
/// already executable. Doing so would put the solver into an infinite loop.
TEST_F(SparsePropagationTest, MarkBlockExecutable) {
Function *F = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "f", &M);
Function *G = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "g", &M);
BasicBlock *FEntry = BasicBlock::Create(Context, "", F);
BasicBlock *GEntry = BasicBlock::Create(Context, "", G);
Builder.SetInsertPoint(FEntry);
Builder.CreateCall(G);
Builder.CreateRetVoid();
Builder.SetInsertPoint(GEntry);
Builder.CreateCall(F);
Builder.CreateRetVoid();
Solver.MarkBlockExecutable(FEntry);
Solver.Solve();
EXPECT_TRUE(Solver.isBlockExecutable(GEntry));
}
/// Test that we propagate information through global variables.
///
/// @gv = internal global i64
///
/// define internal void @f() {
/// store i64 1, i64* @gv
/// ret void
/// }
///
/// define internal void @g() {
/// store i64 1, i64* @gv
/// ret void
/// }
///
/// For this test, we initially mark both "f" and "g" executable, and the
/// solver computes the lattice state of the global variable as constant.
TEST_F(SparsePropagationTest, GlobalVariableConstant) {
Function *F = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "f", &M);
Function *G = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "g", &M);
GlobalVariable *GV =
new GlobalVariable(M, Builder.getInt64Ty(), false,
GlobalValue::InternalLinkage, nullptr, "gv");
BasicBlock *FEntry = BasicBlock::Create(Context, "", F);
BasicBlock *GEntry = BasicBlock::Create(Context, "", G);
Builder.SetInsertPoint(FEntry);
Builder.CreateStore(Builder.getInt64(1), GV);
Builder.CreateRetVoid();
Builder.SetInsertPoint(GEntry);
Builder.CreateStore(Builder.getInt64(1), GV);
Builder.CreateRetVoid();
Solver.MarkBlockExecutable(FEntry);
Solver.MarkBlockExecutable(GEntry);
Solver.Solve();
auto MemGV = TestLatticeKey(GV, IPOGrouping::Memory);
EXPECT_TRUE(Solver.getExistingValueState(MemGV).isConstant());
}
/// Test that we propagate information through global variables.
///
/// @gv = internal global i64
///
/// define internal void @f() {
/// store i64 0, i64* @gv
/// ret void
/// }
///
/// define internal void @g() {
/// store i64 1, i64* @gv
/// ret void
/// }
///
/// For this test, we initially mark both "f" and "g" executable, and the
/// solver computes the lattice state of the global variable as overdefined.
TEST_F(SparsePropagationTest, GlobalVariableOverDefined) {
Function *F = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "f", &M);
Function *G = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "g", &M);
GlobalVariable *GV =
new GlobalVariable(M, Builder.getInt64Ty(), false,
GlobalValue::InternalLinkage, nullptr, "gv");
BasicBlock *FEntry = BasicBlock::Create(Context, "", F);
BasicBlock *GEntry = BasicBlock::Create(Context, "", G);
Builder.SetInsertPoint(FEntry);
Builder.CreateStore(Builder.getInt64(0), GV);
Builder.CreateRetVoid();
Builder.SetInsertPoint(GEntry);
Builder.CreateStore(Builder.getInt64(1), GV);
Builder.CreateRetVoid();
Solver.MarkBlockExecutable(FEntry);
Solver.MarkBlockExecutable(GEntry);
Solver.Solve();
auto MemGV = TestLatticeKey(GV, IPOGrouping::Memory);
EXPECT_TRUE(Solver.getExistingValueState(MemGV).isOverdefined());
}
/// Test that we propagate information through function returns.
///
/// define internal i64 @f(i1* %cond) {
/// if:
/// %0 = load i1, i1* %cond
/// br i1 %0, label %then, label %else
///
/// then:
/// ret i64 1
///
/// else:
/// ret i64 1
/// }
///
/// For this test, we initially mark "f" executable, and the solver computes
/// the return value of the function as constant.
TEST_F(SparsePropagationTest, FunctionDefined) {
Function *F =
Function::Create(FunctionType::get(Builder.getInt64Ty(),
{Type::getInt1PtrTy(Context)}, false),
GlobalValue::InternalLinkage, "f", &M);
BasicBlock *If = BasicBlock::Create(Context, "if", F);
BasicBlock *Then = BasicBlock::Create(Context, "then", F);
BasicBlock *Else = BasicBlock::Create(Context, "else", F);
F->arg_begin()->setName("cond");
Builder.SetInsertPoint(If);
LoadInst *Cond = Builder.CreateLoad(Type::getInt1Ty(Context), F->arg_begin());
Builder.CreateCondBr(Cond, Then, Else);
Builder.SetInsertPoint(Then);
Builder.CreateRet(Builder.getInt64(1));
Builder.SetInsertPoint(Else);
Builder.CreateRet(Builder.getInt64(1));
Solver.MarkBlockExecutable(If);
Solver.Solve();
auto RetF = TestLatticeKey(F, IPOGrouping::Return);
EXPECT_TRUE(Solver.getExistingValueState(RetF).isConstant());
}
/// Test that we propagate information through function returns.
///
/// define internal i64 @f(i1* %cond) {
/// if:
/// %0 = load i1, i1* %cond
/// br i1 %0, label %then, label %else
///
/// then:
/// ret i64 0
///
/// else:
/// ret i64 1
/// }
///
/// For this test, we initially mark "f" executable, and the solver computes
/// the return value of the function as overdefined.
TEST_F(SparsePropagationTest, FunctionOverDefined) {
Function *F =
Function::Create(FunctionType::get(Builder.getInt64Ty(),
{Type::getInt1PtrTy(Context)}, false),
GlobalValue::InternalLinkage, "f", &M);
BasicBlock *If = BasicBlock::Create(Context, "if", F);
BasicBlock *Then = BasicBlock::Create(Context, "then", F);
BasicBlock *Else = BasicBlock::Create(Context, "else", F);
F->arg_begin()->setName("cond");
Builder.SetInsertPoint(If);
LoadInst *Cond = Builder.CreateLoad(Type::getInt1Ty(Context), F->arg_begin());
Builder.CreateCondBr(Cond, Then, Else);
Builder.SetInsertPoint(Then);
Builder.CreateRet(Builder.getInt64(0));
Builder.SetInsertPoint(Else);
Builder.CreateRet(Builder.getInt64(1));
Solver.MarkBlockExecutable(If);
Solver.Solve();
auto RetF = TestLatticeKey(F, IPOGrouping::Return);
EXPECT_TRUE(Solver.getExistingValueState(RetF).isOverdefined());
}
/// Test that we propagate information through arguments.
///
/// define internal void @f() {
/// call void @g(i64 0, i64 1)
/// call void @g(i64 1, i64 1)
/// ret void
/// }
///
/// define internal void @g(i64 %a, i64 %b) {
/// ret void
/// }
///
/// For this test, we initially mark "f" executable, and the solver discovers
/// "g" because of the calls in "f". The solver computes the state of argument
/// "a" as overdefined and the state of "b" as constant.
///
/// In addition, this test demonstrates that ComputeInstructionState can alter
/// the state of multiple lattice values, in addition to the one associated
/// with the instruction definition. Each call instruction in this test updates
/// the state of arguments "a" and "b".
TEST_F(SparsePropagationTest, ComputeInstructionState) {
Function *F = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "f", &M);
Function *G = Function::Create(
FunctionType::get(Builder.getVoidTy(),
{Builder.getInt64Ty(), Builder.getInt64Ty()}, false),
GlobalValue::InternalLinkage, "g", &M);
Argument *A = G->arg_begin();
Argument *B = std::next(G->arg_begin());
A->setName("a");
B->setName("b");
BasicBlock *FEntry = BasicBlock::Create(Context, "", F);
BasicBlock *GEntry = BasicBlock::Create(Context, "", G);
Builder.SetInsertPoint(FEntry);
Builder.CreateCall(G, {Builder.getInt64(0), Builder.getInt64(1)});
Builder.CreateCall(G, {Builder.getInt64(1), Builder.getInt64(1)});
Builder.CreateRetVoid();
Builder.SetInsertPoint(GEntry);
Builder.CreateRetVoid();
Solver.MarkBlockExecutable(FEntry);
Solver.Solve();
auto RegA = TestLatticeKey(A, IPOGrouping::Register);
auto RegB = TestLatticeKey(B, IPOGrouping::Register);
EXPECT_TRUE(Solver.getExistingValueState(RegA).isOverdefined());
EXPECT_TRUE(Solver.getExistingValueState(RegB).isConstant());
}
/// Test that we can handle exceptional terminator instructions.
///
/// declare internal void @p()
///
/// declare internal void @g()
///
/// define internal void @f() personality i8* bitcast (void ()* @p to i8*) {
/// entry:
/// invoke void @g()
/// to label %exit unwind label %catch.pad
///
/// catch.pad:
/// %0 = catchswitch within none [label %catch.body] unwind to caller
///
/// catch.body:
/// %1 = catchpad within %0 []
/// catchret from %1 to label %exit
///
/// exit:
/// ret void
/// }
///
/// For this test, we initially mark the entry block executable. The solver
/// then discovers the rest of the blocks in the function are executable.
TEST_F(SparsePropagationTest, ExceptionalTerminatorInsts) {
Function *P = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "p", &M);
Function *G = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "g", &M);
Function *F = Function::Create(FunctionType::get(Builder.getVoidTy(), false),
GlobalValue::InternalLinkage, "f", &M);
Constant *C =
ConstantExpr::getCast(Instruction::BitCast, P, Builder.getInt8PtrTy());
F->setPersonalityFn(C);
BasicBlock *Entry = BasicBlock::Create(Context, "entry", F);
BasicBlock *Pad = BasicBlock::Create(Context, "catch.pad", F);
BasicBlock *Body = BasicBlock::Create(Context, "catch.body", F);
BasicBlock *Exit = BasicBlock::Create(Context, "exit", F);
Builder.SetInsertPoint(Entry);
Builder.CreateInvoke(G, Exit, Pad);
Builder.SetInsertPoint(Pad);
CatchSwitchInst *CatchSwitch =
Builder.CreateCatchSwitch(ConstantTokenNone::get(Context), nullptr, 1);
CatchSwitch->addHandler(Body);
Builder.SetInsertPoint(Body);
CatchPadInst *CatchPad = Builder.CreateCatchPad(CatchSwitch, {});
Builder.CreateCatchRet(CatchPad, Exit);
Builder.SetInsertPoint(Exit);
Builder.CreateRetVoid();
Solver.MarkBlockExecutable(Entry);
Solver.Solve();
EXPECT_TRUE(Solver.isBlockExecutable(Pad));
EXPECT_TRUE(Solver.isBlockExecutable(Body));
EXPECT_TRUE(Solver.isBlockExecutable(Exit));
}