Skip to content

Commit

Permalink
[clang][dataflow] Add MatchSwitch utility library.
Browse files Browse the repository at this point in the history
Adds `MatchSwitch`, a library for simplifying implementation of transfer
functions. `MatchSwitch` supports constructing a "switch" statement, where each
case of the switch is defined by an AST matcher. The cases are considered in
order, like pattern matching in functional languages.

Differential Revision: https://reviews.llvm.org/D120900
  • Loading branch information
ymand committed Mar 4, 2022
1 parent 4e817b3 commit c88deef
Show file tree
Hide file tree
Showing 3 changed files with 358 additions and 0 deletions.
153 changes: 153 additions & 0 deletions clang/include/clang/Analysis/FlowSensitive/MatchSwitch.h
@@ -0,0 +1,153 @@
//===---- MatchSwitch.h -----------------------------------------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// This file defines the `MatchSwitch` abstraction for building a "switch"
// statement, where each case of the switch is defined by an AST matcher. The
// cases are considered in order, like pattern matching in functional
// languages.
//
// Currently, the design is catered towards simplifying the implementation of
// `DataflowAnalysis` transfer functions. Based on experience here, this
// library may be generalized and moved to ASTMatchers.
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_MATCHSWITCH_H_
#define LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_MATCHSWITCH_H_

#include "clang/AST/ASTContext.h"
#include "clang/AST/Stmt.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "llvm/ADT/StringRef.h"
#include <functional>
#include <string>
#include <utility>
#include <vector>

namespace clang {
namespace dataflow {

/// A common form of state shared between the cases of a transfer function.
template <typename LatticeT> struct TransferState {
TransferState(LatticeT &Lattice, Environment &Env)
: Lattice(Lattice), Env(Env) {}

/// Current lattice element.
LatticeT &Lattice;
Environment &Env;
};

/// Matches against `Stmt` and, based on its structure, dispatches to an
/// appropriate handler.
template <typename State>
using MatchSwitch = std::function<void(const Stmt &, ASTContext &, State &)>;

/// Collects cases of a "match switch": a collection of matchers paired with
/// callbacks, which together define a switch that can be applied to a
/// `Stmt`. This structure can simplify the definition of `transfer` functions
/// that rely on pattern-matching.
///
/// For example, consider an analysis that handles particular function calls. It
/// can define the `MatchSwitch` once, in the constructor of the analysis, and
/// then reuse it each time that `transfer` is called, with a fresh state value.
///
/// \code
/// MatchSwitch<TransferState<MyLattice> BuildSwitch() {
/// return MatchSwitchBuilder<TransferState<MyLattice>>()
/// .CaseOf(callExpr(callee(functionDecl(hasName("foo")))), TransferFooCall)
/// .CaseOf(callExpr(argumentCountIs(2),
/// callee(functionDecl(hasName("bar")))),
/// TransferBarCall)
/// .Build();
/// }
/// \endcode
template <typename State> class MatchSwitchBuilder {
public:
// An action is triggered by the match of a pattern against the input
// statement. For generality, actions take both the matched statement and the
// set of bindings produced by the match.
using Action = std::function<void(
const Stmt *, const ast_matchers::MatchFinder::MatchResult &, State &)>;

MatchSwitchBuilder &&CaseOf(ast_matchers::internal::Matcher<Stmt> M,
Action A) && {
Matchers.push_back(std::move(M));
Actions.push_back(std::move(A));
return std::move(*this);
}

// Convenience function for the common case, where bound nodes are not
// needed. `Node` should be a subclass of `Stmt`.
template <typename Node>
MatchSwitchBuilder &&CaseOf(ast_matchers::internal::Matcher<Stmt> M,
void (*Action)(const Node *, State &)) && {
Matchers.push_back(std::move(M));
Actions.push_back([Action](const Stmt *Stmt,
const ast_matchers::MatchFinder::MatchResult &,
State &S) { Action(cast<Node>(Stmt), S); });
return std::move(*this);
}

MatchSwitch<State> Build() && {
return [Matcher = BuildMatcher(), Actions = std::move(Actions)](
const Stmt &Stmt, ASTContext &Context, State &S) {
auto Results = ast_matchers::matchDynamic(Matcher, Stmt, Context);
if (Results.empty())
return;
// Look through the map for the first binding of the form "TagN..." use
// that to select the action.
for (const auto &Element : Results[0].getMap()) {
llvm::StringRef ID(Element.first);
size_t Index = 0;
if (ID.consume_front("Tag") && !ID.getAsInteger(10, Index) &&
Index < Actions.size()) {
Actions[Index](
&Stmt,
ast_matchers::MatchFinder::MatchResult(Results[0], &Context), S);
return;
}
}
};
}

private:
ast_matchers::internal::DynTypedMatcher BuildMatcher() {
using ast_matchers::anything;
using ast_matchers::stmt;
using ast_matchers::unless;
using ast_matchers::internal::DynTypedMatcher;
if (Matchers.empty())
return stmt(unless(anything()));
for (int I = 0, N = Matchers.size(); I < N; ++I) {
std::string Tag = ("Tag" + llvm::Twine(I)).str();
// Many matchers are not bindable, so ensure that tryBind will work.
Matchers[I].setAllowBind(true);
auto M = *Matchers[I].tryBind(Tag);
// Each anyOf explicitly controls the traversal kind. The anyOf itself is
// set to `TK_AsIs` to ensure no nodes are skipped, thereby deferring to
// the kind of the branches. Then, each branch is either left as is, if
// the kind is already set, or explicitly set to `TK_AsIs`. We choose this
// setting because it is the default interpretation of matchers.
Matchers[I] =
!M.getTraversalKind() ? M.withTraversalKind(TK_AsIs) : std::move(M);
}
// The matcher type on the cases ensures that `Expr` kind is compatible with
// all of the matchers.
return DynTypedMatcher::constructVariadic(
DynTypedMatcher::VO_AnyOf, ASTNodeKind::getFromNodeKind<Stmt>(),
std::move(Matchers));
}

std::vector<ast_matchers::internal::DynTypedMatcher> Matchers;
std::vector<Action> Actions;
};
} // namespace dataflow
} // namespace clang
#endif // LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_MATCHSWITCH_H_
1 change: 1 addition & 0 deletions clang/unittests/Analysis/FlowSensitive/CMakeLists.txt
Expand Up @@ -7,6 +7,7 @@ add_clang_unittest(ClangAnalysisFlowSensitiveTests
DataflowAnalysisContextTest.cpp
DataflowEnvironmentTest.cpp
MapLatticeTest.cpp
MatchSwitchTest.cpp
MultiVarConstantPropagationTest.cpp
SingleVarConstantPropagationTest.cpp
SourceLocationsLatticeTest.cpp
Expand Down
204 changes: 204 additions & 0 deletions clang/unittests/Analysis/FlowSensitive/MatchSwitchTest.cpp
@@ -0,0 +1,204 @@
//===- unittests/Analysis/FlowSensitive/MatchSwitchTest.cpp ---------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// This file defines a simplistic version of Constant Propagation as an example
// of a forward, monotonic dataflow analysis. The analysis tracks all
// variables in the scope, but lacks escape analysis.
//
//===----------------------------------------------------------------------===//

#include "clang/Analysis/FlowSensitive/MatchSwitch.h"
#include "TestingSupport.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Expr.h"
#include "clang/AST/Stmt.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
#include "clang/Analysis/FlowSensitive/MapLattice.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/None.h"
#include "llvm/ADT/Optional.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/Error.h"
#include "llvm/Testing/Support/Annotations.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <cstdint>
#include <memory>
#include <ostream>
#include <string>
#include <utility>

using namespace clang;
using namespace dataflow;

namespace {
using ::testing::Pair;
using ::testing::UnorderedElementsAre;

class BooleanLattice {
public:
BooleanLattice() : Value(false) {}
explicit BooleanLattice(bool B) : Value(B) {}

static BooleanLattice bottom() { return BooleanLattice(false); }

static BooleanLattice top() { return BooleanLattice(true); }

LatticeJoinEffect join(BooleanLattice Other) {
auto Prev = Value;
Value = Value || Other.Value;
return Prev == Value ? LatticeJoinEffect::Unchanged
: LatticeJoinEffect::Changed;
}

friend bool operator==(BooleanLattice LHS, BooleanLattice RHS) {
return LHS.Value == RHS.Value;
}

friend std::ostream &operator<<(std::ostream &Os, const BooleanLattice &B) {
Os << B.Value;
return Os;
}

bool value() const { return Value; }

private:
bool Value;
};
} // namespace

MATCHER_P(Holds, m,
((negation ? "doesn't hold" : "holds") +
llvm::StringRef(" a lattice element that ") +
::testing::DescribeMatcher<BooleanLattice>(m, negation))
.str()) {
return ExplainMatchResult(m, arg.Lattice, result_listener);
}

void TransferSetTrue(const DeclRefExpr *,
TransferState<BooleanLattice> &State) {
State.Lattice = BooleanLattice(true);
}

void TransferSetFalse(const Stmt *,
const ast_matchers::MatchFinder::MatchResult &,
TransferState<BooleanLattice> &State) {
State.Lattice = BooleanLattice(false);
}

class TestAnalysis : public DataflowAnalysis<TestAnalysis, BooleanLattice> {
MatchSwitch<TransferState<BooleanLattice>> TransferSwitch;

public:
explicit TestAnalysis(ASTContext &Context)
: DataflowAnalysis<TestAnalysis, BooleanLattice>(Context) {
using namespace ast_matchers;
TransferSwitch =
MatchSwitchBuilder<TransferState<BooleanLattice>>()
.CaseOf(declRefExpr(to(varDecl(hasName("X")))), TransferSetTrue)
.CaseOf(callExpr(callee(functionDecl(hasName("Foo")))),
TransferSetFalse)
.Build();
}

static BooleanLattice initialElement() { return BooleanLattice::bottom(); }

void transfer(const Stmt *S, BooleanLattice &L, Environment &Env) {
TransferState<BooleanLattice> State(L, Env);
TransferSwitch(*S, getASTContext(), State);
}
};

class MatchSwitchTest : public ::testing::Test {
protected:
template <typename Matcher>
void RunDataflow(llvm::StringRef Code, Matcher Expectations) {
ASSERT_THAT_ERROR(
test::checkDataflow<TestAnalysis>(
Code, "fun",
[](ASTContext &C, Environment &) { return TestAnalysis(C); },
[&Expectations](
llvm::ArrayRef<std::pair<
std::string, DataflowAnalysisState<TestAnalysis::Lattice>>>
Results,
ASTContext &) { EXPECT_THAT(Results, Expectations); },
{"-fsyntax-only", "-std=c++17"}),
llvm::Succeeded());
}
};

TEST_F(MatchSwitchTest, JustX) {
std::string Code = R"(
void fun() {
int X = 1;
(void)X;
// [[p]]
}
)";
RunDataflow(Code,
UnorderedElementsAre(Pair("p", Holds(BooleanLattice(true)))));
}

TEST_F(MatchSwitchTest, JustFoo) {
std::string Code = R"(
void Foo();
void fun() {
Foo();
// [[p]]
}
)";
RunDataflow(Code,
UnorderedElementsAre(Pair("p", Holds(BooleanLattice(false)))));
}

TEST_F(MatchSwitchTest, XThenFoo) {
std::string Code = R"(
void Foo();
void fun() {
int X = 1;
(void)X;
Foo();
// [[p]]
}
)";
RunDataflow(Code,
UnorderedElementsAre(Pair("p", Holds(BooleanLattice(false)))));
}

TEST_F(MatchSwitchTest, FooThenX) {
std::string Code = R"(
void Foo();
void fun() {
Foo();
int X = 1;
(void)X;
// [[p]]
}
)";
RunDataflow(Code,
UnorderedElementsAre(Pair("p", Holds(BooleanLattice(true)))));
}

TEST_F(MatchSwitchTest, Neither) {
std::string Code = R"(
void Bar();
void fun(bool b) {
Bar();
// [[p]]
}
)";
RunDataflow(Code,
UnorderedElementsAre(Pair("p", Holds(BooleanLattice(false)))));
}

0 comments on commit c88deef

Please sign in to comment.