Files
revng-revng/include/revng/PTML/CommentPlacementHelper.h
2026-02-13 08:41:39 +02:00

449 lines
17 KiB
C++

#pragma once
//
// This file is distributed under the MIT License. See LICENSE.md for details.
//
#include "llvm/ADT/DepthFirstIterator.h"
#include "llvm/ADT/SmallBitVector.h"
#include "llvm/IR/Dominators.h"
#include "llvm/Support/GenericDomTree.h"
#include "revng/ADT/RecursiveCoroutine.h"
#include "revng/GraphLayout/Traits.h"
#include "revng/Model/Function.h"
#include "revng/PTML/Doxygen.h"
namespace yield {
template<typename NodeType>
struct StatementTraits {
// See `HasStatementTraits` for the list of what this trait should provide.
};
template<typename Traits, typename NodeType>
concept IsStatementTraitsFor = requires(NodeType Node,
Traits::StatementType Statement) {
// using StatementType = /* your type */
typename Traits::StatementType;
// static RangeOf<StatementType> auto getStatements(NodeType);
{ Traits::getStatements(Node) } -> RangeOf<typename Traits::StatementType>;
// static RangeOf<MetaAddress> auto getAddresses(StatementType);
{ Traits::getAddresses(Statement) } -> RangeOf<MetaAddress>;
};
template<typename NodeType>
concept HasStatementTraits = IsStatementTraitsFor<StatementTraits<NodeType>,
NodeType>;
template<typename Traits, typename NodeType>
concept IsTreeTraitsFor = requires {
typename Traits::TreeType;
typename Traits::TreeNodeType;
requires layout::HasLLVMGraphTraits<typename Traits::TreeNodeType>;
{
Traits::getTreeRoot(std::declval<const typename Traits::TreeType &>())
} -> std::same_as<typename Traits::TreeNodeType>;
{
Traits::getNode(std::declval<const typename Traits::TreeNodeType &>())
} -> std::convertible_to<NodeType>;
};
template<typename Graph, typename Traits, typename NodeType>
concept IsTreeTraitsCompatibleGraph = //
IsTreeTraitsFor<Traits, NodeType> and requires {
{
Traits::getTree(std::declval<Graph &>())
} -> std::same_as<typename Traits::TreeType>;
};
} // namespace yield
namespace llvm {
// These are necessary for llvm graph helpers (such as `llvm::depth_first`)
// to be usable on dominator trees.
//
// Luckily, we don't need to implement them from scratch, since
// `llvm::DomTreeNodeBase` already does so (under a different name),
// we just need to point in the right direction.
template<typename NodeType>
requires yield::HasStatementTraits<NodeType *>
struct GraphTraits<llvm::DomTreeNodeBase<NodeType> *>
: public DomTreeGraphTraitsBase<
llvm::DomTreeNodeBase<NodeType>,
typename llvm::DomTreeNodeBase<NodeType>::const_iterator> {};
template<typename NodeType>
requires yield::HasStatementTraits<NodeType *>
struct GraphTraits<const llvm::DomTreeNodeBase<NodeType> *>
: public DomTreeGraphTraitsBase<
const llvm::DomTreeNodeBase<NodeType>,
typename llvm::DomTreeNodeBase<NodeType>::const_iterator> {};
} // namespace llvm
namespace yield {
template<typename NodeT>
struct DominatorTreeTraits {
private:
using NodeType = std::remove_pointer_t<NodeT>;
public:
using TreeType = llvm::DominatorTreeBase<NodeType, false>;
using TreeNodeType = const llvm::DomTreeNodeBase<NodeType> *;
static TreeType getTree(auto &&Graph) {
// Compute a dominator tree for the given graph to reduce duplicate
// comments: if a suitable statement is already dominated by another one,
// we can skip that emission.
TreeType Tree;
Tree.recalculate(Graph);
return Tree;
}
static TreeNodeType getTreeRoot(const TreeType &Tree) {
return Tree.getRootNode();
}
static NodeType *getNode(TreeNodeType Node) { return Node->getBlock(); }
};
/// This is a helper for deciding which statement (within a given graph) is
/// best suited for emission of a given comment.
///
/// \tparam NodeType the type of the graph node.
/// Note that it has to specialize `StatementTraits`.
///
/// A typical usage would be something like:
///
/// ```cpp
/// // 1. Make a helper object from your function and your graph:
/// CommentPlacementHelper<MyNodeType> CommentPlacement(MyModelFunction,
/// MyGraph);
/// // note: We need to be able to compute the dominator tree. In order to allow
/// // that, you need to provide a `llvm::DomTreeNodeTraits<MyNodeType>`
/// // trait OR `MyNodeType::getParent()` method.
///
/// // 2. When emitting your artifact, add calls to `getComments`:
/// for (const auto &Block : myWayOfIteratingTheGraph(MyGraph)) {
/// for (const auto &Statement : myWayOfGettingTheStatements(Block))
/// // . . .
/// for (const auto &Comment : CommentPlacement.getComments(Statement)) {
/// emitTheComment(Comment);
/// // note: see `PTML/Doxygen` for the standard way of emitting comments
/// }
/// // . . .
/// }
/// }
///
/// // 3. Don't forget to emit homeless comments somewhere.
/// // These are the comments that do not have even a single match among
/// // all the statements in the provided graph.
/// for (const auto &Comment : CommentPlacement.getHomelessComments())
/// emitTheComment(Comment);
/// ```
///
/// With the interface out of the way, lets now discuss *how* statements are
/// selected:
///
/// 1. For each comment-statement pair a similarity score is computed
/// (see \ref tverskyIndex) based on the similarity of the set of addresses
/// recorded in the comment to the set of addresses associated to
/// the statement.
/// 2. Then, for each comment a single *address set* (note: not a statement yet)
/// is selected.
/// 3. Then graph is traversed again, this time looking for all the statements
/// whose location exactly matches the selected one.
/// When there's an exact match, the comment is assigned to that statement.
/// This happens *unless* the current statement is dominated by another
/// statement that is already associated to the comment currently being
/// considered. This prevents emitting the same comment multiple times
/// in cases where it's obviously not beneficial.
/// 4. All these statements are recorded in a \ref ResultMap and will be emitted
/// once requested by a \ref getComments call.
///
/// For some specific examples on how this helper works, see the corresponding
/// unit tests, but let me also provide a trivial example. Let's say we have
/// a *really* sequential graph with three nodes: `(A) -> (B) -> (C)`.
/// And let's say each node has a single statement with addresses attached:
/// - A: { 0x001:Generic64 }
/// - B: { 0x001:Generic64, 0x002:Generic64 }
/// - C: { 0x001:Generic64, 0x002:Generic64 }
///
/// And let's say we're trying to decide where to emit a comment with
/// `{ 0x001:Generic64, 0x002:Generic64, 0x003:Generic64 }`
///
/// After running step 1 (above) on it, the resulting scores for locations(!)
/// are as follows (note that there are only two locations):
/// - `{ 0x001:Generic64 }`: 1/3
/// - `{ 0x001:Generic64, 0x002:Generic64 }`: 2/3
///
/// So the best one is selected. And there are two statements that correspond
/// to it: `B` and `C`.
///
/// If these two statement were not related, the comment would end up duplicated
/// BUT based on the graph, the only way to reach `C` is through `B`! As such,
/// there's not much downside to suppressing the `C` comment and only emitting
/// one before `B`.
template<typename NodeType,
IsTreeTraitsFor<NodeType> TreeTrait,
IsStatementTraitsFor<NodeType> StmtTrait = StatementTraits<NodeType>>
class CommentPlacementHelper {
private:
using StatementType = typename StmtTrait::StatementType;
using SLT = decltype(StmtTrait::getAddresses(std::declval<StatementType>()));
using StatementLocationType = std::decay_t<SLT>;
struct Score {
uint64_t Numerator = 0;
uint64_t Denominator = 1;
std::strong_ordering operator<=>(const Score &Another) const {
// This takes advantage of the easiest way to compare two fractions:
// instead of looking for the *smallest* common denominator,
// just use *one* common denominator: product of the two denominators.
//
// Say, we're comparing `a/b` and `c/d`: *one* common denominator is
// `b*d`, which means we can just compare `(a*d)/(b*d)` and `(b*c)/(b*d)`.
// And since "adjusted" denominators are the same, we can also omit them
// and just compare the numerators.
uint64_t AdjustedLHS = this->Numerator * Another.Denominator;
uint64_t AdjustedRHS = Another.Numerator * this->Denominator;
return AdjustedLHS <=> AdjustedRHS;
}
bool operator==(const Score &Another) const = default;
};
public:
struct CommentAssignment {
/// Since we emit comments as `@comment(i64 INDEX, ...)` (using index
/// instead of the comment text allows us to avoid invalidating any IR on
/// a comment edit since it's only read by the backend) we have to report
/// the index to the callee.
///
/// Also note that it's trivial to go `index -> reference`
/// (`MyFunction.Comments().at(MyIndex)`), but `reference -> index` is not
/// possible (`Comments` is a normal vector and we place no limitation
/// on duplicate elements).
uint64_t CommentIndex = 0;
bool LocationMatchesExactly = false;
Score Score = {};
const model::StatementComment::TypeOfLocation *ExpectedLocation = nullptr;
};
using CommentList = std::vector<CommentAssignment>;
private:
// This assumes that `StmtTrait::StatementType` is cheap (and, more
// importantly, safe) to copy
std::unordered_map<StatementType, CommentList> ResultMap;
CommentList HomelessComments;
public:
CommentPlacementHelper() = default;
template<IsTreeTraitsCompatibleGraph<TreeTrait, NodeType> GraphT>
CommentPlacementHelper(const model::Function &Function, GraphT &&Graph) {
if (Function.Comments().empty()) {
// No comments in this function, nothing to map
return;
}
auto Tree = TreeTrait::getTree(Graph);
std::vector<std::pair<Score, StatementLocationType>> Scores;
Scores.resize(Function.Comments().size());
// First build a score map - select the best location for each comment
for (auto Node : llvm::depth_first(TreeTrait::getTreeRoot(Tree))) {
for (const auto &Statement :
StmtTrait::getStatements(TreeTrait::getNode(Node))) {
StatementLocationType Location = StmtTrait::getAddresses(Statement);
for (auto &&Comment : Function.Comments()) {
// Use the Tversky Index as the scoring function.
// Note the parameters controlling the weights of the sets: the higher
// a value is, the bigger the relative negative impact of
// the corresponding set having *extra* elements compared to the other
// one.
//
// We want this because a line having unaccounted for addresses is a
// way bigger red flag that a (potentially user edited) comment having
// them.
auto Score = tverskyIndex<1, 2>(Comment.Location(), Location);
if (Score.has_value())
if (shouldReplace(Scores[Comment.Index()], { *Score, Location }))
Scores[Comment.Index()] = { *Score, Location };
}
}
}
// Process homeless comments so they don't interfere with the others
for (auto &&[I, Comment] : llvm::enumerate(Function.Comments()))
if (Scores[I].first.Numerator == 0)
HomelessComments.emplace_back(I, false, Score{}, &Comment.Location());
TreeVisitor Visitor{ ResultMap, Function, Scores };
Visitor(TreeTrait::getTreeRoot(Tree));
}
private:
struct TreeVisitor {
using TreeNodeType = typename TreeTrait::TreeNodeType;
std::unordered_map<StatementType, CommentList> &ResultMap;
const model::Function &Function;
const std::vector<std::pair<Score, StatementLocationType>> &Scores;
void operator()(TreeNodeType RootNode) {
impl(RootNode, llvm::SmallBitVector(Scores.size()));
}
private:
RecursiveCoroutine<> impl(TreeNodeType Node,
llvm::SmallBitVector AssignedInThisBranch) {
for (const auto &Statement :
StmtTrait::getStatements(TreeTrait::getNode(Node))) {
StatementLocationType Location = StmtTrait::getAddresses(Statement);
for (auto &&Comment : Function.Comments()) {
if (AssignedInThisBranch.test(Comment.Index()))
continue;
if (Location.empty()) {
// Skip nodes without locations - they should never have comments
// assigned to them.
continue;
}
if (Location == Scores[Comment.Index()].second) {
// A node with a non-empty location matched the selected score,
// mark the node as a target for the emission.
//
// Note that comments with empty `Scores[Comment.Index()].second`
// are homeless.
bool IsLocationExact = std::ranges::equal(Comment.Location(),
Scores[Comment.Index()]
.second);
ResultMap[Statement].emplace_back(Comment.Index(),
IsLocationExact,
Scores[Comment.Index()].first,
&Comment.Location());
// Mark this comment as 'skipped' for all the dominated statements.
AssignedInThisBranch.set(Comment.Index());
}
}
}
// Proceed on the children with a copy of the `AssignedInThisBranch` map
// so that adjacent children don't affect each other.
for (const auto &Ch : llvm::children<TreeNodeType>(Node))
rc_recur impl(Ch, llvm::SmallBitVector{ AssignedInThisBranch });
}
};
private:
bool shouldReplace(std::pair<Score, StatementLocationType> From,
std::pair<Score, StatementLocationType> To) {
const auto &[FromScore, FromLocation] = From;
const auto &[ToScore, ToLocation] = To;
// Never replace anything with an empty score
if (ToScore.Numerator == 0 || ToLocation.empty())
return false;
// Always replace an empty score
if (FromScore.Numerator == 0 || FromLocation.empty())
return true;
// If scores differ, that's all we need
if (ToScore != FromScore)
return ToScore > FromScore;
// If scores are indistinguishable, compare locations lexicographically.
//
// Note that this will return `false` if both locations are identical, but
// we're fine with that as in that case both of them will get the comment
// unless one of them dominates the other.
auto Comparator = [](const MetaAddress &Left, const MetaAddress &Right) {
return Left.address() < Right.address();
};
return std::ranges::lexicographical_compare(ToLocation,
FromLocation,
Comparator);
}
public:
const CommentList &getComments(StmtTrait::StatementType Node) const {
if (auto Iterator = ResultMap.find(Node); Iterator != ResultMap.end())
return Iterator->second;
static CommentList Empty{};
return Empty;
}
const CommentList &getHomelessComments() const { return HomelessComments; }
private:
static uint64_t relativeComplementSize(std::ranges::range auto &&LHS,
std::ranges::range auto &&RHS) {
return std::ranges::count_if(LHS, [&RHS](auto &&Element) {
return !llvm::is_contained(RHS, Element);
});
}
static uint64_t intersectionSize(std::ranges::range auto &&LHS,
std::ranges::range auto &&RHS) {
return std::ranges::count_if(LHS, [&RHS](auto &&Element) {
return llvm::is_contained(RHS, Element);
});
}
/// Tversky Index is a more general version of the Jaccard Index commonly
/// used for evaluation of similarity of set pairs.
///
/// \tparam Alpha weight of the first set
/// \tparam Beta weight of the second set
///
/// \note If `Alpha == Beta == 1` this produces Jaccard Index.
///
/// \note If `Alpha != Beta`, the index is not symmetric, as such, take care
/// to pass LHS and RHS in the exact same order each time.
///
/// \param LHS first set, does *not* have to be sorted.
/// \param RHS second set, does *not* have to be sorted.
template<uint64_t Alpha = 1, uint64_t Beta = 1>
static std::optional<Score>
tverskyIndex(std::ranges::range auto &&LHS, std::ranges::range auto &&RHS) {
uint64_t IntersectionSize = intersectionSize(LHS, RHS);
if (IntersectionSize == 0) {
// No point proceeding, there's no match.
return std::nullopt;
}
uint64_t LHSComplementSize = relativeComplementSize(LHS, RHS);
uint64_t RHSComplementSize = relativeComplementSize(RHS, LHS);
return Score{ .Numerator = IntersectionSize,
.Denominator = IntersectionSize + Alpha * LHSComplementSize
+ Beta * RHSComplementSize };
}
};
template<typename NodeType,
IsStatementTraitsFor<NodeType> StmtTrait = StatementTraits<NodeType>>
using DTCommentPlacementHelper = CommentPlacementHelper<
NodeType,
DominatorTreeTraits<NodeType>,
StmtTrait>;
} // namespace yield