mamba/libmamba/src/core/satisfiability_error.cpp

1787 lines
70 KiB
C++

// Copyright (c) 2019, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <regex>
#include <vector>
#include <algorithm>
#include <map>
#include <type_traits>
#include <string_view>
#include <functional>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <limits>
#include <solv/pool.h>
#include <fmt/color.h>
#include <fmt/ostream.h>
#include <fmt/format.h>
#include "mamba/core/output.hpp"
#include "mamba/core/util_string.hpp"
#include "mamba/core/satisfiability_error.hpp"
#include "mamba/core/package_info.hpp"
#include "mamba/core/solver.hpp"
#include "mamba/core/pool.hpp"
namespace mamba
{
/**************************************
* Implementation of DependencyInfo *
**************************************/
DependencyInfo::DependencyInfo(const std::string& dep)
{
static std::regex const regexp("\\s*(\\w[\\w-]*)\\s*([^\\s]*)(?:\\s+([^\\s]+))?\\s*");
std::smatch matches;
bool const matched = std::regex_match(dep, matches, regexp);
// First match is the whole regex match
if (!matched || matches.size() != 4)
{
throw std::runtime_error("Invalid dependency info: " + dep);
}
m_name = matches.str(1);
m_version_range = matches.str(2);
if (m_version_range.empty())
{
m_version_range = "*";
}
m_build_range = matches.str(3);
if (m_build_range.empty())
{
m_build_range = "*";
}
}
std::string const& DependencyInfo::name() const
{
return m_name;
}
std::string const& DependencyInfo::version() const
{
return m_version_range;
}
std::string const& DependencyInfo::build_string() const
{
return m_build_range;
}
std::string DependencyInfo::str() const
{
std::string out(m_name);
out.reserve(m_name.size() + (m_version_range.empty() ? 0 : 1) + m_version_range.size()
+ (m_build_range.empty() ? 0 : 1) + m_version_range.size());
if (!m_version_range.empty())
{
out += ' ';
out += m_version_range;
}
if (!m_build_range.empty())
{
out += ' ';
out += m_build_range;
}
return out;
}
bool DependencyInfo::operator==(DependencyInfo const& other) const
{
auto attrs = [](DependencyInfo const& x)
{ return std::tie(x.name(), x.version(), x.build_string()); };
return attrs(*this) == attrs(other);
}
/*************************************
* Implementation of ProblemsGraph *
*************************************/
namespace
{
void warn_unexpected_problem(MSolverProblem const& problem)
{
// TODO: Once the new error message are not experimental, we should consider
// reducing this level since it is not somethig the user has control over.
LOG_WARNING << "Unexpected empty optionals for problem type "
<< solver_ruleinfo_name(problem.type);
}
class ProblemsGraphCreator
{
public:
using SolvId = Id; // Unscoped from libsolv
using graph_t = ProblemsGraph::graph_t;
using RootNode = ProblemsGraph::RootNode;
using PackageNode = ProblemsGraph::PackageNode;
using UnresolvedDependencyNode = ProblemsGraph::UnresolvedDependencyNode;
using ConstraintNode = ProblemsGraph::ConstraintNode;
using node_t = ProblemsGraph::node_t;
using node_id = ProblemsGraph::node_id;
using edge_t = ProblemsGraph::edge_t;
using conflicts_t = ProblemsGraph::conflicts_t;
ProblemsGraphCreator(MSolver const& solver, MPool const& pool)
: m_solver{ solver }
, m_pool{ pool }
{
m_root_node = m_graph.add_node(RootNode());
parse_problems();
}
operator ProblemsGraph() &&
{
return { std::move(m_graph), std::move(m_conflicts), m_root_node };
}
private:
MSolver const& m_solver;
MPool const& m_pool;
graph_t m_graph;
conflicts_t m_conflicts;
std::map<SolvId, node_id> m_solv2node;
node_id m_root_node;
/**
* Add a node and return the node id.
*
* If the node is already present and ``update`` is false then the current
* node is left as it is, otherwise the new value is inserted.
*/
node_id add_solvable(SolvId solv_id, node_t&& pkg_info, bool update = true);
void add_conflict(node_id n1, node_id n2);
[[nodiscard]] bool add_expanded_deps_edges(node_id from_id,
SolvId dep_id,
edge_t const& edge);
void parse_problems();
};
auto ProblemsGraphCreator::add_solvable(SolvId solv_id, node_t&& node, bool update)
-> node_id
{
if (auto const iter = m_solv2node.find(solv_id); iter != m_solv2node.end())
{
node_id const id = iter->second;
if (update)
{
m_graph.node(id) = std::move(node);
}
return id;
}
node_id const id = m_graph.add_node(std::move(node));
m_solv2node[solv_id] = id;
return id;
};
void ProblemsGraphCreator::add_conflict(node_id n1, node_id n2)
{
m_conflicts.add(n1, n2);
}
bool ProblemsGraphCreator::add_expanded_deps_edges(node_id from_id,
SolvId dep_id,
edge_t const& edge)
{
bool added = false;
for (const auto& solv_id : m_pool.select_solvables(dep_id))
{
added = true;
PackageInfo pkg_info(pool_id2solvable(m_pool, solv_id));
node_id to_id = add_solvable(
solv_id, PackageNode{ std::move(pkg_info), std::nullopt }, false);
m_graph.add_edge(from_id, to_id, edge);
}
return added;
}
void ProblemsGraphCreator::parse_problems()
{
for (auto& problem : m_solver.all_problems_structured())
{
std::optional<PackageInfo>& source = problem.source;
std::optional<PackageInfo>& target = problem.target;
std::optional<std::string>& dep = problem.dep;
SolverRuleinfo type = problem.type;
switch (type)
{
case SOLVER_RULE_PKG_CONSTRAINS:
{
// A constraint (run_constrained) on source is conflicting with target.
// SOLVER_RULE_PKG_CONSTRAINS has a dep, but it can resolve to nothing.
// The constraint conflict is actually expressed between the target and
// a constrains node child of the source.
if (!source || !target || !dep)
{
warn_unexpected_problem(problem);
break;
}
auto src_id
= add_solvable(problem.source_id,
PackageNode{ std::move(source).value(), std::nullopt });
node_id tgt_id = add_solvable(
problem.target_id, PackageNode{ std::move(target).value(), { type } });
node_id cons_id
= add_solvable(problem.dep_id, ConstraintNode{ dep.value() });
DependencyInfo edge(dep.value());
m_graph.add_edge(src_id, cons_id, std::move(edge));
add_conflict(cons_id, tgt_id);
break;
}
case SOLVER_RULE_PKG_REQUIRES:
{
// Express a dependency on source that is involved in explaining the
// problem.
// Not all dependency of package will appear, only enough to explain the
// problem. It is not a problem in itself, only a part of the graph.
if (!dep || !source)
{
warn_unexpected_problem(problem);
break;
}
auto src_id
= add_solvable(problem.source_id,
PackageNode{ std::move(source).value(), std::nullopt });
DependencyInfo edge(dep.value());
bool added = add_expanded_deps_edges(src_id, problem.dep_id, edge);
if (!added)
{
LOG_WARNING << "Added empty dependency for problem type "
<< solver_ruleinfo_name(type);
}
break;
}
case SOLVER_RULE_JOB:
case SOLVER_RULE_PKG:
{
// A top level requirement.
// The difference between JOB and PKG is unknown (possibly unused).
if (!dep)
{
warn_unexpected_problem(problem);
break;
}
DependencyInfo edge(dep.value());
bool added = add_expanded_deps_edges(m_root_node, problem.dep_id, edge);
if (!added)
{
LOG_WARNING << "Added empty dependency for problem type "
<< solver_ruleinfo_name(type);
}
break;
}
case SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP:
case SOLVER_RULE_JOB_UNKNOWN_PACKAGE:
{
// A top level dependency does not exist.
// Could be a wrong name or missing channel.
if (!dep)
{
warn_unexpected_problem(problem);
break;
}
DependencyInfo edge(dep.value());
node_id dep_id = add_solvable(
problem.dep_id,
UnresolvedDependencyNode{ std::move(dep).value(), type });
m_graph.add_edge(m_root_node, dep_id, std::move(edge));
break;
}
case SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP:
{
// A package dependency does not exist.
// Could be a wrong name or missing channel.
// This is a partial exaplanation of why a specific solvable (could be any
// of the parent) cannot be installed.
if (!source || !dep)
{
warn_unexpected_problem(problem);
break;
}
DependencyInfo edge(dep.value());
node_id src_id
= add_solvable(problem.source_id,
PackageNode{ std::move(source).value(), std::nullopt });
node_id dep_id = add_solvable(
problem.dep_id,
UnresolvedDependencyNode{ std::move(dep).value(), type });
m_graph.add_edge(src_id, dep_id, std::move(edge));
break;
}
case SOLVER_RULE_PKG_CONFLICTS:
case SOLVER_RULE_PKG_SAME_NAME:
{
// Looking for a valid solution to the installation satisfiability expand to
// two solvables of same package that cannot be installed together. This is
// a partial exaplanation of why one of the solvables (could be any of the
// parent) cannot be installed.
if (!source || !target)
{
warn_unexpected_problem(problem);
break;
}
node_id src_id = add_solvable(
problem.source_id, PackageNode{ std::move(source).value(), { type } });
node_id tgt_id = add_solvable(
problem.target_id, PackageNode{ std::move(target).value(), { type } });
add_conflict(src_id, tgt_id);
break;
}
case SOLVER_RULE_UPDATE:
{
// Encounterd in the problems list from libsolv but unknown.
// Explicitly ignored until we do something with it.
break;
}
default:
{
// Many more SolverRuleinfo that heve not been encountered.
LOG_WARNING << "Problem type not implemented "
<< solver_ruleinfo_name(type);
break;
}
}
};
}
}
auto ProblemsGraph::from_solver(MSolver const& solver, MPool const& pool) -> ProblemsGraph
{
return ProblemsGraphCreator(solver, pool);
}
ProblemsGraph::ProblemsGraph(graph_t graph, conflicts_t conflicts, node_id root_node)
: m_graph(std::move(graph))
, m_conflicts(std::move(conflicts))
, m_root_node(root_node)
{
}
auto ProblemsGraph::graph() const noexcept -> graph_t const&
{
return m_graph;
}
auto ProblemsGraph::conflicts() const noexcept -> conflicts_t const&
{
return m_conflicts;
}
auto ProblemsGraph::root_node() const noexcept -> node_id
{
return m_root_node;
}
/***********************************************
* Implementation of CompressedProblemsGraph *
***********************************************/
namespace
{
using old_node_id_list = std::vector<ProblemsGraph::node_id>;
/**
* Merge node indices for the a given type of node.
*
* This function is applied among node of the same type to compute the indices of nodes
* that will be merged together.
*
* @param node_indices The indices of nodes of a given type.
* @param merge_criteria A binary function that decices whether two nodes should be merged
* together. The function is assumed to be symetric and transitive.
* @return A partition of the the indices in @p node_indices.
*/
template <typename CompFunc>
auto merge_node_indices_for_one_node_type(
std::vector<ProblemsGraph::node_id> const& node_indices, CompFunc&& merge_criteria)
-> std::vector<old_node_id_list>
{
using node_id = ProblemsGraph::node_id;
std::vector<old_node_id_list> groups{};
std::size_t const n_nodes = node_indices.size();
std::vector<bool> node_added_to_a_group(n_nodes, false);
for (std::size_t i = 0; i < n_nodes; ++i)
{
if (!node_added_to_a_group[i])
{
auto const id_i = node_indices[i];
std::vector<node_id> current_group{};
current_group.push_back(id_i);
node_added_to_a_group[i] = true;
// This is where we use symetry and transitivity, going through all remaining
// nodes and adding them to the current group if they match the criteria.
for (std::size_t j = i + 1; j < n_nodes; ++j)
{
auto const id_j = node_indices[j];
if ((!node_added_to_a_group[j]) && merge_criteria(id_i, id_j))
{
current_group.push_back(id_j);
node_added_to_a_group[j] = true;
}
}
groups.push_back(std::move(current_group));
}
}
assert(std::all_of(node_added_to_a_group.begin(),
node_added_to_a_group.end(),
[](auto x) { return x; }));
return groups;
}
/**
* Used to organize objects by node type (the index of the type in the variant).
*/
template <typename T>
using node_type_list = std::vector<T>;
/**
* Group indices of variants with same alternative together.
*
* If a variant at index ``i`` in the input @p vrnts holds alternative with index ``k``
* then the output will contain ``i`` in position ``k``.
*/
template <typename... T>
auto variant_by_index(std::vector<std::variant<T...>> const& vrnts)
-> node_type_list<std::vector<std::size_t>>
{
auto out = node_type_list<std::vector<std::size_t>>(sizeof...(T));
auto const n = vrnts.size();
for (std::size_t i = 0; i < n; ++i)
{
out[vrnts[i].index()].push_back(i);
}
return out;
}
/**
* Merge node indices together.
*
* Merge by applying the @p merge_criteria to nodes that hold the same type in the variant.
*
* @param merge_criteria A binary function that decices whether two nodes should be merged
* together. The function is assumed to be symetric and transitive.
* @return For each node type, a partition of the the indices in @p of that type..
*/
template <typename CompFunc>
auto merge_node_indices(ProblemsGraph::graph_t::node_list const& nodes,
CompFunc&& merge_criteria)
-> node_type_list<std::vector<old_node_id_list>>
{
auto merge_func = [&merge_criteria](auto const& node_indices_of_one_node_type)
{
return merge_node_indices_for_one_node_type(node_indices_of_one_node_type,
std::forward<CompFunc>(merge_criteria));
};
auto const nodes_by_type = variant_by_index(nodes);
node_type_list<std::vector<old_node_id_list>> groups(nodes_by_type.size());
std::transform(nodes_by_type.begin(), nodes_by_type.end(), groups.begin(), merge_func);
return groups;
}
/**
* Given a variant type and a type, return the index of that type in the variant.
*
* Conceptually the opposite of ``std::variant_alternative``.
* Credits to Nykakin on StackOverflow (https://stackoverflow.com/a/52303671).
*/
template <typename VariantType, typename T, std::size_t index = 0>
constexpr std::size_t variant_type_index()
{
static_assert(std::variant_size_v<VariantType> > index, "Type not found in variant");
if constexpr (std::is_same_v<std::variant_alternative_t<index, VariantType>, T>)
{
return index;
}
else
{
return variant_type_index<VariantType, T, index + 1>();
}
}
/**
* ``ranges::transform(f) | ranges::to<CompressedProblemsGraph::NamedList>()``
*/
template <typename Range, typename Func>
auto transform_to_list(Range const& rng, Func&& f)
{
using T = typename Range::value_type;
using O = std::invoke_result_t<Func, T>;
// TODO(C++20) ranges::view::transform
auto tmp = std::vector<O>();
tmp.reserve(rng.size());
std::transform(rng.begin(), rng.end(), std::back_inserter(tmp), std::forward<Func>(f));
return CompressedProblemsGraph::NamedList<O>(tmp.begin(), tmp.end());
}
/**
* Detect if a type has a ``name`` member (function).
*/
template <class T, class = void>
struct has_name : std::false_type
{
};
template <class T>
struct has_name<T, std::void_t<decltype(std::invoke(&T::name, std::declval<T>()))>>
: std::true_type
{
};
template <typename T>
inline constexpr bool has_name_v = has_name<T>::value;
template <typename T, typename Str>
decltype(auto) name_or(T const& obj, Str val)
{
if constexpr (has_name_v<T>)
{
return std::invoke(&T::name, obj);
}
else
{
return val;
}
}
/**
* The name of a ProblemsGraph::node_t, used to avoid merging.
*/
std::string_view node_name(ProblemsGraph::node_t const& node)
{
return std::visit([](auto const& n) -> std::string_view { return name_or(n, ""); },
node);
}
/**
* The criteria for deciding whether to merge two nodes together.
*/
auto default_merge_criteria(ProblemsGraph const& pbs,
ProblemsGraph::node_id n1,
ProblemsGraph::node_id n2) -> bool
{
using node_id = ProblemsGraph::node_id;
auto const& g = pbs.graph();
auto is_leaf = [&g](node_id n) -> bool { return g.successors(n).size() == 0; };
auto leaves_from = [&g](node_id n) -> vector_set<node_id>
{
auto leaves = std::vector<node_id>();
g.for_each_leaf_from(n, [&leaves](node_id m) { leaves.push_back(m); });
return vector_set(std::move(leaves));
};
return (node_name(g.node(n1)) == node_name(g.node(n2)))
// Merging conflicts would be counter-productive in explaining problems
&& !(pbs.conflicts().in_conflict(n1, n2))
// We don't want to use leaves_from for leaves because it resove to themselves,
// preventing any merging.
&& ((is_leaf(n1) && is_leaf(n2)) || (leaves_from(n1) == leaves_from(n2)))
// We only check the parents for non-leaves meaning parents can "inject"
// themselves into a bigger problem
&& ((!is_leaf(n1) && !is_leaf(n2))
|| (g.predecessors(n1) == g.predecessors(n2)));
}
using node_id_mapping = std::vector<CompressedProblemsGraph::node_id>;
/**
* For a given type of node, merge nodes together and add them to the new graph.
*
* @param old_graph The graph containing the node data referenced by @p old_groups.
* @param old_groups A partition of the node indices to merge together.
* @param new_graph The graph where the merged nodes are added.
* @param old_to_new A mapping of old node indices to new node indices updated with the new
* nodes merged.
*/
template <typename Node>
void merge_nodes_for_one_node_type(ProblemsGraph::graph_t const& old_graph,
std::vector<old_node_id_list> const& old_groups,
CompressedProblemsGraph::graph_t& new_graph,
node_id_mapping& old_to_new)
{
// Check nothrow move for efficient push_back
static_assert(std::is_nothrow_move_constructible_v<Node>);
auto get_old_node = [&old_graph](ProblemsGraph::node_id id)
{
auto node = old_graph.node(id);
// Must always be the case that old_groups only references node ids of type Node
assert(std::holds_alternative<Node>(node));
return std::get<Node>(std::move(node));
};
for (auto const& old_grp : old_groups)
{
auto const new_id = new_graph.add_node(transform_to_list(old_grp, get_old_node));
for (auto const old_id : old_grp)
{
old_to_new[old_id] = new_id;
}
}
}
/**
* Merge nodes together.
*
* Merge by applying the @p merge_criteria to nodes that hold the same type in the variant.
*
* @param merge_criteria A binary function that decices whether two nodes should be merged
* together. The function is assumed to be symetric and transitive.
* @return A tuple of the graph with newly created nodes (without edges), the new root node,
* and a mapping between old node ids and new node ids.
*/
template <typename CompFunc>
auto merge_nodes(ProblemsGraph const& pbs, CompFunc&& merge_criteria)
-> std::tuple<CompressedProblemsGraph::graph_t,
CompressedProblemsGraph::node_id,
node_id_mapping>
{
auto const& old_graph = pbs.graph();
auto new_graph = CompressedProblemsGraph::graph_t();
auto const new_root_node = new_graph.add_node(CompressedProblemsGraph::RootNode());
auto old_to_new
= std::vector<CompressedProblemsGraph::node_id>(old_graph.number_of_nodes());
auto old_ids_groups
= merge_node_indices(old_graph.nodes(), std::forward<CompFunc>(merge_criteria));
{
using Node = ProblemsGraph::RootNode;
[[maybe_unused]] static constexpr auto type_idx
= variant_type_index<ProblemsGraph::node_t, Node>();
assert(old_ids_groups[type_idx].size() == 1);
assert(old_ids_groups[type_idx][0].size() == 1);
assert(old_ids_groups[type_idx][0][0] == pbs.root_node());
old_to_new[pbs.root_node()] = new_root_node;
}
{
using Node = ProblemsGraph::PackageNode;
static constexpr auto type_idx = variant_type_index<ProblemsGraph::node_t, Node>();
merge_nodes_for_one_node_type<Node>(
old_graph, old_ids_groups[type_idx], new_graph, old_to_new);
}
{
using Node = ProblemsGraph::UnresolvedDependencyNode;
static constexpr auto type_idx = variant_type_index<ProblemsGraph::node_t, Node>();
merge_nodes_for_one_node_type<Node>(
old_graph, old_ids_groups[type_idx], new_graph, old_to_new);
}
{
using Node = ProblemsGraph::ConstraintNode;
static constexpr auto type_idx = variant_type_index<ProblemsGraph::node_t, Node>();
merge_nodes_for_one_node_type<Node>(
old_graph, old_ids_groups[type_idx], new_graph, old_to_new);
}
return std::tuple{ std::move(new_graph), new_root_node, std::move(old_to_new) };
}
/**
* For all groups in the new graph, merge the edges in-between nodes of those groups.
*
* @param old_graph The graph with the nodes use for merging.
* @param new_graph The graph with nodes already merged, modified to add new edges.
* @param old_to_new A mapping between old node ids and new node ids.
*/
void merge_edges(ProblemsGraph::graph_t const& old_graph,
CompressedProblemsGraph::graph_t& new_graph,
node_id_mapping const& old_to_new)
{
// Check nothrow move for efficient push_back
static_assert(std::is_nothrow_move_constructible_v<ProblemsGraph::edge_t>);
auto add_new_edge = [&](ProblemsGraph::node_id old_from, ProblemsGraph::node_id old_to)
{
auto const new_from = old_to_new[old_from];
auto const new_to = old_to_new[old_to];
if (!new_graph.has_edge(new_from, new_to))
{
new_graph.add_edge(new_from, new_to, CompressedProblemsGraph::edge_t());
}
new_graph.edge(new_from, new_to).insert(old_graph.edge(old_from, old_to));
};
old_graph.for_each_edge(add_new_edge);
}
/**
* Merge the conflicts according to the new groups of nodes.
*
* If two groups contain a node that are respectively in conflicts, then they are in
* conflicts.
*/
auto merge_conflicts(ProblemsGraph::conflicts_t const& old_conflicts,
node_id_mapping const& old_to_new)
-> CompressedProblemsGraph::conflicts_t
{
auto new_conflicts = CompressedProblemsGraph::conflicts_t();
for (auto const& [old_from, old_with] : old_conflicts)
{
auto const new_from = old_to_new[old_from];
for (auto const old_to : old_with)
{
new_conflicts.add(new_from, old_to_new[old_to]);
}
}
return new_conflicts;
}
}
// TODO move graph nodes and edges.
auto CompressedProblemsGraph::from_problems_graph(ProblemsGraph const& pbs,
merge_criteria_t const& merge_criteria)
-> CompressedProblemsGraph
{
graph_t graph;
node_id root_node;
node_id_mapping old_to_new;
if (merge_criteria)
{
auto merge_func
= [&pbs, &merge_criteria](ProblemsGraph::node_id n1, ProblemsGraph::node_id n2)
{ return merge_criteria(pbs, n1, n2); };
std::tie(graph, root_node, old_to_new) = merge_nodes(pbs, merge_func);
}
else
{
auto merge_func = [&pbs](ProblemsGraph::node_id n1, ProblemsGraph::node_id n2)
{ return default_merge_criteria(pbs, n1, n2); };
std::tie(graph, root_node, old_to_new) = merge_nodes(pbs, merge_func);
}
merge_edges(pbs.graph(), graph, old_to_new);
auto conflicts = merge_conflicts(pbs.conflicts(), old_to_new);
return { std::move(graph), std::move(conflicts), root_node };
}
CompressedProblemsGraph::CompressedProblemsGraph(graph_t graph,
conflicts_t conflicts,
node_id root_node)
: m_graph(std::move(graph))
, m_conflicts(std::move(conflicts))
, m_root_node(root_node)
{
}
auto CompressedProblemsGraph::graph() const noexcept -> graph_t const&
{
return m_graph;
}
auto CompressedProblemsGraph::conflicts() const noexcept -> conflicts_t const&
{
return m_conflicts;
}
auto CompressedProblemsGraph::root_node() const noexcept -> node_id
{
return m_root_node;
}
/*************************************************************
* Implementation of CompressedProblemsGraph::RoughCompare *
*************************************************************/
template <>
bool CompressedProblemsGraph::RoughCompare<ProblemsGraph::PackageNode>::operator()(
ProblemsGraph::PackageNode const& a, ProblemsGraph::PackageNode const& b)
{
auto attrs = [](ProblemsGraph::PackageNode const& x)
{ return std::tie(x.name, x.version, x.build_number, x.build_string); };
return attrs(a) < attrs(b);
}
template <typename T>
bool CompressedProblemsGraph::RoughCompare<T>::operator()(T const& a, T const& b)
{
auto attrs = [](DependencyInfo const& x)
{ return std::tie(x.name(), x.version(), x.build_string()); };
return attrs(a) < attrs(b);
}
template struct CompressedProblemsGraph::RoughCompare<ProblemsGraph::PackageNode>;
template struct CompressedProblemsGraph::RoughCompare<ProblemsGraph::UnresolvedDependencyNode>;
template struct CompressedProblemsGraph::RoughCompare<ProblemsGraph::ConstraintNode>;
template struct CompressedProblemsGraph::RoughCompare<DependencyInfo>;
/**********************************************************
* Implementation of CompressedProblemsGraph::NamedList *
**********************************************************/
namespace
{
template <typename T>
decltype(auto) invoke_name(T&& e)
{
using TT = std::remove_cv_t<std::remove_reference_t<T>>;
return std::invoke(&TT::name, std::forward<T>(e));
}
}
template <typename T, typename A>
template <typename InputIterator>
CompressedProblemsGraph::NamedList<T, A>::NamedList(InputIterator first, InputIterator last)
{
if (first < last)
{
for (auto it = first; it < last; ++it)
{
if (invoke_name(*it) != invoke_name(*first))
{
throw std::invalid_argument(concat("iterator contains different names (",
invoke_name(*first),
", ",
invoke_name(*it)));
}
}
}
Base::insert(first, last);
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::front() const noexcept -> value_type const&
{
return Base::front();
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::back() const noexcept -> value_type const&
{
return Base::back();
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::begin() const noexcept -> const_iterator
{
return Base::begin();
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::end() const noexcept -> const_iterator
{
return Base::end();
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::rbegin() const noexcept -> const_reverse_iterator
{
return Base::rbegin();
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::rend() const noexcept -> const_reverse_iterator
{
return Base::rend();
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::name() const -> std::string const&
{
if (size() == 0)
{
static const std::string empty = "";
return empty;
}
return invoke_name(front());
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::versions_trunc(std::string_view sep,
std::string_view etc,
std::size_t threshold,
bool remove_duplicates) const
-> std::pair<std::string, std::size_t>
{
auto versions = std::vector<std::string>(size());
auto invoke_version = [](auto&& v) -> decltype(auto)
{
using TT = std::remove_cv_t<std::remove_reference_t<decltype(v)>>;
return std::invoke(&TT::version, std::forward<decltype(v)>(v));
};
// TODO(C++20) *this | std::ranges::transform(invoke_version) | ranges::unique
std::transform(begin(), end(), versions.begin(), invoke_version);
if (remove_duplicates)
{
versions.erase(std::unique(versions.begin(), versions.end()), versions.end());
}
return { join_trunc(versions, sep, etc, threshold), versions.size() };
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::build_strings_trunc(std::string_view sep,
std::string_view etc,
std::size_t threshold,
bool remove_duplicates) const
-> std::pair<std::string, std::size_t>
{
auto builds = std::vector<std::string>(size());
auto invoke_build_string = [](auto&& v) -> decltype(auto)
{
using TT = std::remove_cv_t<std::remove_reference_t<decltype(v)>>;
return std::invoke(&TT::build_string, std::forward<decltype(v)>(v));
};
// TODO(C++20) *this | std::ranges::transform(invoke_buid_string) | ranges::unique
std::transform(begin(), end(), builds.begin(), invoke_build_string);
if (remove_duplicates)
{
builds.erase(std::unique(builds.begin(), builds.end()), builds.end());
}
return { join_trunc(builds, sep, etc, threshold), builds.size() };
}
template <typename T, typename A>
auto CompressedProblemsGraph::NamedList<T, A>::versions_and_build_strings_trunc(
std::string_view sep,
std::string_view etc,
std::size_t threshold,
bool remove_duplicates) const -> std::pair<std::string, std::size_t>
{
auto versions_builds = std::vector<std::string>(size());
auto invoke_version_builds = [](auto&& v) -> decltype(auto)
{
using TT = std::remove_cv_t<std::remove_reference_t<decltype(v)>>;
return fmt::format("{} {}",
std::invoke(&TT::version, std::forward<decltype(v)>(v)),
std::invoke(&TT::build_string, std::forward<decltype(v)>(v)));
};
// TODO(C++20) *this | std::ranges::transform(invoke_version) | ranges::unique
std::transform(begin(), end(), versions_builds.begin(), invoke_version_builds);
if (remove_duplicates)
{
versions_builds.erase(std::unique(versions_builds.begin(), versions_builds.end()),
versions_builds.end());
}
return { join_trunc(versions_builds, sep, etc, threshold), versions_builds.size() };
}
template <typename T, typename A>
void CompressedProblemsGraph::NamedList<T, A>::insert(value_type const& e)
{
return insert_impl(e);
}
template <typename T, typename A>
void CompressedProblemsGraph::NamedList<T, A>::insert(value_type&& e)
{
return insert_impl(std::move(e));
}
template <typename T, typename A>
template <typename T_>
void CompressedProblemsGraph::NamedList<T, A>::insert_impl(T_&& e)
{
if ((size() > 0) && (invoke_name(e) != name()))
{
throw std::invalid_argument("Name of new element (" + invoke_name(e)
+ ") does not match name of list (" + name() + ')');
}
Base::insert(std::forward<T_>(e));
}
template class CompressedProblemsGraph::NamedList<ProblemsGraph::PackageNode>;
template class CompressedProblemsGraph::NamedList<ProblemsGraph::UnresolvedDependencyNode>;
template class CompressedProblemsGraph::NamedList<ProblemsGraph::ConstraintNode>;
template class CompressedProblemsGraph::NamedList<DependencyInfo>;
/***********************************
* Implementation of summary_msg *
***********************************/
std::ostream& print_problem_summary_msg(std::ostream& out, CompressedProblemsGraph const& pbs)
{
return out << "Could not solve for environment specs\n";
}
std::string problem_summary_msg(CompressedProblemsGraph const& pbs)
{
std::stringstream ss;
print_problem_summary_msg(ss, pbs);
return ss.str();
}
/****************************************
* Implementation of problem_tree_msg *
****************************************/
namespace
{
/**
* Describes how a given graph node appears in the DFS.
*/
struct TreeNode
{
enum struct Type
{
/** A root node with no ancestors and at least one successor. */
root,
/** A leaf node with at least one ancestors and no successor. */
leaf,
/** A node with at least one ancestor that has already been visited */
visited,
/** Start dependency split (multiple edges with same dep_id). */
split,
/** A regular node with at least one ancestor and at least one successor. */
diving
};
/**
* Keep track of whether a node is the last visited among its sibling.
*/
enum struct SiblingNumber : bool
{
not_last = 0,
last = 1,
};
/** Progagate a status up the tree, such as whether the package is installable. */
using Status = bool;
using node_id = CompressedProblemsGraph::node_id;
std::vector<SiblingNumber> ancestry;
/** Multiple node_id means that the node is "virtual".
*
* It represents a group of node, as in a split (but other types are used as well).
*/
std::vector<node_id> ids;
std::vector<node_id> ids_from;
Type type;
Type type_from;
Status status;
auto depth() const -> std::size_t
{
return ancestry.size();
}
};
/**
* The depth first search (DFS) algorithm to explain problems.
*
* We need to reimplement a DFS algorithm instead of using the one provided by the one
* from DiGraph because we need a more complex exploration, including:
* - Controling the order in which neighbors are explored;
* - Dynamically adding and removing nodes;
* - Executing some operations before and after a node is visited;
* - Propagating information from the exploration of the subtree back to the current
* node (e.g. the status).
*
* The goal of this class is to return a vector of ``TreeNode``, *i.e.* a ``node_id``
* enhanced with extra DFS information.
* This is not where string representation is done.
*
* A first step in mitigating these constraints would be to put more structure in the
* graph to start with.
* Currently a node ``pkg_a-0.1.0-build`` depends directly on other packages such as
* - ``pkg_b-0.3.0-build``
* - ``pkg_b-0.3.1-build``
* - ``pkg_c-0.2.0-build``
* One and only one of ``pkg_b`` and ``pkg_c`` must be selected, which is why we have
* to first do a grouping by package name.
* A more structured approach would be to put an intermerdiary "dependency" node
* (currently represented by the edge data) that would serve as grouping packages with the
* same name together.
*/
class TreeDFS
{
public:
/**
* Initialize search data and capture reference to the problems.
*/
TreeDFS(CompressedProblemsGraph const& pbs);
/**
* Execute DFS and return the vector of ``TreeNode``.
*/
auto explore() -> std::vector<TreeNode>;
private:
using graph_t = CompressedProblemsGraph::graph_t;
using node_id = CompressedProblemsGraph::node_id;
using Status = TreeNode::Status;
using SiblingNumber = TreeNode::SiblingNumber;
using TreeNodeList = std::vector<TreeNode>;
using TreeNodeIter = typename TreeNodeList::iterator;
vector_set<node_id> leaf_installables = {};
std::vector<std::optional<Status>> m_node_visited;
CompressedProblemsGraph const& m_pbs;
/**
* Function to decide if a node is uninstallable.
*
* For a leaf this sets the final status.
* For other nodes, a pacakge could still be uninstallable because of its children.
*/
auto node_uninstallable(node_id id) -> Status;
/**
* Get the type of a node depending on the exploration.
*/
auto node_type(node_id id) const -> TreeNode::Type;
/**
* The successors of a node, grouped by same dependency name (edge data).
*/
auto successors_per_dep(node_id from, bool name_only);
/**
* Visit a "split" node.
*
* A node that aims at grouping versions and builds of a given dependency.
* Exactly the missing information that should be added to the graph as a proper node.
*/
auto visit_split(std::vector<node_id> const& children_ids,
SiblingNumber position,
TreeNode const& from,
TreeNodeIter out) -> std::pair<TreeNodeIter, Status>;
/**
* Visit a node from another node.
*/
auto visit_node(node_id id,
SiblingNumber position,
TreeNode const& from,
TreeNodeIter out) -> std::pair<TreeNodeIter, Status>;
/**
* Visit the first node in the graph.
*/
auto visit_node(node_id id, TreeNodeIter out) -> std::pair<TreeNodeIter, Status>;
/**
* Code reuse.
*/
auto visit_node_impl(node_id id, TreeNode const& ongoing, TreeNodeIter out)
-> std::pair<TreeNodeIter, Status>;
};
/*******************************
* Implementation of TreeDFS *
*******************************/
TreeDFS::TreeDFS(CompressedProblemsGraph const& pbs)
: m_node_visited(pbs.graph().number_of_nodes(), std::nullopt)
, m_pbs(pbs)
{
}
auto TreeDFS::explore() -> std::vector<TreeNode>
{
// Using the number of edges as an upper bound on the number of split nodes inserted
auto path = std::vector<TreeNode>(m_pbs.graph().number_of_edges()
+ m_pbs.graph().number_of_nodes());
auto [out, _] = visit_node(m_pbs.root_node(), path.begin());
path.resize(out - path.begin());
return path;
}
auto TreeDFS::node_uninstallable(node_id id) -> Status
{
auto installables_contains = [&](auto&& id) { return leaf_installables.contains(id); };
auto const& conflicts = m_pbs.conflicts();
// Conflicts are tricky to handle because they are not an isolated problem, they only
// appear in conjunction with another leaf.
// The first time we see a conflict, we add it to the "installables" and return true.
// Otherwise (if the conflict contains a node in conflict with a node that is
// "installable"), we return false.
if (conflicts.has_conflict(id))
{
auto const& conflict_with = conflicts.conflicts(id);
if (std::any_of(conflict_with.begin(), conflict_with.end(), installables_contains))
{
return true;
}
leaf_installables.insert(id);
return false;
}
// Assuming any other type of leave is a kind of problem and other nodes not.
return m_pbs.graph().successors(id).size() == 0;
}
auto TreeDFS::node_type(node_id id) const -> TreeNode::Type
{
bool const has_predecessors = m_pbs.graph().predecessors(id).size() > 0;
bool const has_successors = m_pbs.graph().successors(id).size() > 0;
bool const is_visited = m_node_visited[id].has_value();
// We purposefully check if the node is a leaf before checking if it
// is visited because showing a single node again is more intelligible than
// refering to another one.
if (!has_successors)
{
return TreeNode::Type::leaf;
}
else if (is_visited)
{
return TreeNode::Type::visited;
}
else
{
return has_predecessors ? TreeNode::Type::diving : TreeNode::Type::root;
}
}
auto TreeDFS::successors_per_dep(node_id from, bool name_only)
{
// The key are sorted by alphabetical order of the dependency name
auto out = std::map<std::string, std::vector<node_id>>();
for (auto to : m_pbs.graph().successors(from))
{
auto const& edge = m_pbs.graph().edge(from, to);
std::string key = edge.name();
if (!name_only)
{
// Making up an arbitrary string represnetation of the edge
key += edge.versions_and_build_strings_trunc(
"", "", std::numeric_limits<std::size_t>::max())
.first;
}
out[key].push_back(to);
}
return out;
}
/**
* Specific concatenation for a const vector and a value.
*/
template <typename T, typename U>
auto concat(std::vector<T> const& v, U&& x) -> std::vector<T>
{
auto out = std::vector<T>();
out.reserve(v.size() + 1);
out.insert(out.begin(), v.begin(), v.end());
out.emplace_back(std::forward<U>(x));
return out;
}
auto TreeDFS::visit_split(std::vector<node_id> const& children_ids,
SiblingNumber position,
TreeNode const& from,
TreeNodeIter out) -> std::pair<TreeNodeIter, Status>
{
auto& ongoing = *(out++);
// There is no single node_id for this dynamically created node, we use the vector
// to represent all nodes.
assert(children_ids.size() > 0);
ongoing = TreeNode{
/* .ancestry= */ concat(from.ancestry, position),
/* .ids= */ children_ids,
/* .ids_from= */ from.ids,
/* .type= */ TreeNode::Type::split,
/* .type_from= */ from.type,
/* .status= */ false, // Placeholder updated
};
TreeNodeIter const children_begin = out;
// TODO(C++20) an enumerate view ``views::zip(views::iota(), children_ids)``
std::size_t const n_children = children_ids.size();
for (std::size_t i = 0; i < n_children; ++i)
{
bool const last = (i == n_children - 1);
auto const child_pos = last ? SiblingNumber::last : SiblingNumber::not_last;
Status status;
std::tie(out, status) = visit_node(children_ids[i], child_pos, ongoing, out);
// If there are any valid option in the split, the split is iself valid.
ongoing.status |= status;
}
// All children are the same type of visited or leaves, no grand-children,
// and same status.
// We dynamically delete all children and mark the whole node as such.
auto all_same_split_children = [](TreeNodeIter first, TreeNodeIter last) -> bool
{
if (last <= first)
{
return true;
}
auto same = [&first](TreeNode const& tn)
{ return (tn.type == first->type) && (tn.status == first->status); };
return std::all_of(first, last, same);
};
TreeNodeIter const children_end = out;
if ((n_children >= 1) && all_same_split_children(children_begin, children_end))
{
ongoing.type = children_begin->type;
out = children_begin;
}
return { out, ongoing.status };
}
auto TreeDFS::visit_node(node_id root_id, TreeNodeIter out)
-> std::pair<TreeNodeIter, Status>
{
auto& ongoing = *(out++);
ongoing = TreeNode{
/* .ancestry= */ {},
/* .ids= */ { root_id },
/* .ids_from= */ { root_id },
/* .type= */ node_type(root_id),
/* .type_from= */ node_type(root_id),
/* .status= */ {}, // Placeholder updated
};
auto out_status = visit_node_impl(root_id, ongoing, out);
ongoing.status = out_status.second;
return out_status;
}
auto TreeDFS::visit_node(node_id id,
SiblingNumber position,
TreeNode const& from,
TreeNodeIter out) -> std::pair<TreeNodeIter, Status>
{
auto& ongoing = *(out++);
ongoing = TreeNode{
/* .ancestry= */ concat(from.ancestry, position),
/* .ids= */ { id },
/* .ids_from= */ from.ids,
/* .type= */ node_type(id),
/* .type_from= */ from.type,
/* .status= */ {}, // Placeholder updated
};
auto out_status = visit_node_impl(id, ongoing, out);
ongoing.status = out_status.second;
return out_status;
}
auto TreeDFS::visit_node_impl(node_id id, TreeNode const& ongoing, TreeNodeIter out)
-> std::pair<TreeNodeIter, Status>
{
// At depth 0, we use a stric grouping of edges to avoid gathering user requirements
// that have the same name (e.g. a mistake "python=3.7" "python=3.8").
auto const successors = successors_per_dep(id, ongoing.depth() > 0);
if (auto const status = m_node_visited[id]; status.has_value())
{
return { out, status.value() };
}
if (node_uninstallable(id))
{
m_node_visited[id] = false;
return { out, false };
}
Status status = true;
// TODO(C++20) an enumerate view ``views::zip(views::iota(), children_ids)``
std::size_t i = 0;
for (auto const& [_, children] : successors)
{
auto const children_pos
= i == successors.size() - 1 ? SiblingNumber::last : SiblingNumber::not_last;
Status child_status;
if (children.size() > 1)
{
std::tie(out, child_status) = visit_split(children, children_pos, ongoing, out);
}
else
{
std::tie(out, child_status)
= visit_node(children[0], children_pos, ongoing, out);
}
// All children statuses need to be valid for a parent to be valid.
status &= child_status;
++i;
}
m_node_visited[id] = status;
return { out, status };
}
/**
* Transform a path generated by ``TreeDFS`` into a string representation.
*/
class TreeExplainer
{
public:
static auto explain(std::ostream& outs,
CompressedProblemsGraph const& pbs,
ProblemsMessageFormat const& format,
std::vector<TreeNode> const& path) -> std::ostream&;
private:
using Status = TreeNode::Status;
using SiblingNumber = TreeNode::SiblingNumber;
using node_id = CompressedProblemsGraph::node_id;
using node_t = CompressedProblemsGraph::node_t;
using edge_t = CompressedProblemsGraph::edge_t;
std::ostream& m_outs;
CompressedProblemsGraph const& m_pbs;
ProblemsMessageFormat const& m_format;
TreeExplainer(std::ostream& outs,
CompressedProblemsGraph const& pbs,
ProblemsMessageFormat const& format);
template <typename... Args>
void write(Args&&... args);
void write_ancestry(std::vector<SiblingNumber> const& ancestry);
void write_pkg_list(TreeNode const& tn);
void write_pkg_dep(TreeNode const& tn);
void write_pkg_repr(TreeNode const& tn);
void write_root(TreeNode const& tn);
void write_diving(TreeNode const& tn);
void write_split(TreeNode const& tn);
void write_leaf(TreeNode const& tn);
void write_visited(TreeNode const& tn);
void write_path(std::vector<TreeNode> const& path);
template <typename Node>
auto concat_nodes_impl(std::vector<node_id> const& ids) -> Node;
auto concat_nodes(std::vector<node_id> const& ids) -> node_t;
auto concat_edges(std::vector<node_id> const& from, std::vector<node_id> const& to)
-> edge_t;
};
/*************************************
* Implementation of TreeExplainer *
*************************************/
TreeExplainer::TreeExplainer(std::ostream& outs,
CompressedProblemsGraph const& pbs,
ProblemsMessageFormat const& format)
: m_outs(outs)
, m_pbs(pbs)
, m_format(format)
{
}
template <typename... Args>
void TreeExplainer::write(Args&&... args)
{
(m_outs << ... << std::forward<Args>(args));
}
void TreeExplainer::write_ancestry(std::vector<SiblingNumber> const& ancestry)
{
std::size_t const size = ancestry.size();
auto const indents = m_format.indents;
if (size > 0)
{
for (std::size_t i = 0; i < size - 1; ++i)
{
write(indents[static_cast<std::size_t>(ancestry[i])]);
}
write(indents[2 + static_cast<std::size_t>(ancestry[size - 1])]);
}
}
void TreeExplainer::write_pkg_list(TreeNode const& tn)
{
auto do_write = [&](auto const& node)
{
using Node = std::remove_cv_t<std::remove_reference_t<decltype(node)>>;
if constexpr (!std::is_same_v<Node, CompressedProblemsGraph::RootNode>)
{
auto const style = tn.status ? m_format.available : m_format.unavailable;
auto [versions_trunc, size] = node.versions_trunc();
write(fmt::format(
style, (size == 1 ? "{} {}" : "{} [{}]"), node.name(), versions_trunc));
}
};
std::visit(do_write, concat_nodes(tn.ids));
}
void TreeExplainer::write_pkg_dep(TreeNode const& tn)
{
auto edges = concat_edges(tn.ids_from, tn.ids);
auto const style = tn.status ? m_format.available : m_format.unavailable;
// We show the build string in pkg_dep and not pkg_list because hand written build
// string are more likely to contain vital information about the variant.
auto [vers_builds_trunc, size] = edges.versions_and_build_strings_trunc();
write(fmt::format(
style, (size == 1 ? "{} {}" : "{} [{}]"), edges.name(), vers_builds_trunc));
}
void TreeExplainer::write_pkg_repr(TreeNode const& tn)
{
if (tn.ids_from.size() > 1)
{
assert(tn.depth() > 1);
write_pkg_list(tn);
}
// Node is virtual, represent multiple nodes (like a split)
else
{
write_pkg_dep(tn);
}
}
void TreeExplainer::write_root(TreeNode const& tn)
{
assert(tn.ids.size() == 1); // The root is always a single node
if (m_pbs.graph().successors(tn.ids.front()).size() > 1)
{
write("The following packages are incompatible");
}
else
{
write("The following package could not be installed");
}
}
void TreeExplainer::write_diving(TreeNode const& tn)
{
write_pkg_repr(tn);
if (tn.depth() == 1)
{
if (tn.status)
{
write(" is installable and it requires");
}
else
{
write(" is uninstallable because it requires");
}
}
else if (tn.type_from == TreeNode::Type::split)
{
write(" would require");
}
else
{
write(", which requires");
}
}
void TreeExplainer::write_split(TreeNode const& tn)
{
write_pkg_repr(tn);
if (tn.status)
{
if (tn.depth() == 1)
{
write(" is installable with the potential options");
}
else
{
write(" with the potential options");
}
}
else
{
if (tn.depth() == 1)
{
write(" is uninstallable because there are no viable options");
}
else
{
write(" but there are no viable options");
}
}
}
void TreeExplainer::write_leaf(TreeNode const& tn)
{
auto do_write = [&](auto const& node)
{
using Node = std::remove_cv_t<std::remove_reference_t<decltype(node)>>;
using RootNode = CompressedProblemsGraph::RootNode;
using PackageListNode = CompressedProblemsGraph::PackageListNode;
using UnresolvedDepListNode = CompressedProblemsGraph::UnresolvedDependencyListNode;
using ConstraintListNode = CompressedProblemsGraph::ConstraintListNode;
if constexpr (std::is_same_v<Node, RootNode>)
{
assert(false);
}
else if constexpr (std::is_same_v<
Node,
PackageListNode> || std::is_same_v<Node, ConstraintListNode>)
{
write_pkg_repr(tn);
if (tn.status)
{
if (tn.depth() == 1)
{
write(" is requested and can be installed");
}
else
{
write(", which can be installed");
}
}
else
{
// Assuming this is always a conflict.
if (tn.depth() == 1)
{
write(" is uninstallable because it");
}
else if (tn.type_from != TreeNode::Type::split)
{
write(", which");
}
write(" conflicts with any installable versions previously reported");
}
}
else if constexpr (std::is_same_v<Node, UnresolvedDepListNode>)
{
write_pkg_repr(tn);
if ((tn.depth() > 1) && (tn.type_from != TreeNode::Type::split))
{
write(", which");
}
// Virtual package
if (starts_with(node.name(), "__"))
{
write(" is missing on the system");
}
else
{
write(" does not exsist (perhaps ",
(tn.depth() == 1 ? "a typo or a " : "a "),
"missing channel)");
}
}
};
std::visit(do_write, concat_nodes(tn.ids));
}
void TreeExplainer::write_visited(TreeNode const& tn)
{
write_pkg_repr(tn);
if (tn.status)
{
write(", which can be installed (as previously explained)");
}
else
{
write(", which cannot be installed (as previously explained)");
}
}
void TreeExplainer::write_path(std::vector<TreeNode> const& path)
{
std::size_t const length = path.size();
for (std::size_t i = 0; i < length; ++i)
{
bool const last = (i == length - 1);
auto const& tn = path[i];
write_ancestry(tn.ancestry);
switch (tn.type)
{
case (TreeNode::Type::root):
{
write_root(tn);
write(last ? "." : "\n");
break;
}
case (TreeNode::Type::diving):
{
write_diving(tn);
write(last ? "." : "\n");
break;
}
case (TreeNode::Type::split):
{
write_split(tn);
write(last ? "." : "\n");
break;
}
case (TreeNode::Type::leaf):
{
write_leaf(tn);
write(last ? "." : ";\n");
break;
}
case (TreeNode::Type::visited):
{
write_visited(tn);
write(last ? "." : ";\n");
break;
}
}
}
}
auto TreeExplainer::explain(std::ostream& outs,
CompressedProblemsGraph const& pbs,
ProblemsMessageFormat const& format,
std::vector<TreeNode> const& path) -> std::ostream&
{
auto explainer = TreeExplainer(outs, pbs, format);
explainer.write_path(path);
return outs;
}
template <typename Node>
auto TreeExplainer::concat_nodes_impl(std::vector<node_id> const& ids) -> Node
{
Node out = {};
for (auto id : ids)
{
auto const& node = std::get<Node>(m_pbs.graph().node(id));
out.insert(node.begin(), node.end());
}
return out;
}
auto TreeExplainer::concat_nodes(std::vector<node_id> const& ids) -> node_t
{
assert(ids.size() > 0);
assert(std::all_of(ids.begin(),
ids.end(),
[&](auto id) {
return m_pbs.graph().node(ids.front()).index()
== m_pbs.graph().node(id).index();
}));
return std::visit(
[&](auto const& node) -> node_t
{
using Node = std::remove_cv_t<std::remove_reference_t<decltype(node)>>;
if constexpr (std::is_same_v<Node, CompressedProblemsGraph::RootNode>)
{
return CompressedProblemsGraph::RootNode();
}
else
{
return concat_nodes_impl<Node>(ids);
}
},
m_pbs.graph().node(ids.front()));
}
auto TreeExplainer::concat_edges(std::vector<node_id> const& from,
std::vector<node_id> const& to) -> edge_t
{
auto out = edge_t{};
for (auto f : from)
{
for (auto t : to)
{
auto const& e = m_pbs.graph().edge(f, t);
out.insert(e.begin(), e.end());
}
}
return out;
}
}
std::ostream& print_problem_tree_msg(std::ostream& out,
CompressedProblemsGraph const& pbs,
ProblemsMessageFormat const& format)
{
auto dfs = TreeDFS(pbs);
auto path = dfs.explore();
TreeExplainer::explain(out, pbs, format, path);
return out;
}
std::string problem_tree_msg(CompressedProblemsGraph const& pbs,
ProblemsMessageFormat const& format)
{
std::stringstream ss;
print_problem_tree_msg(ss, pbs);
return ss.str();
}
}