defmodule HexSolver.PartialSolution do
@moduledoc false
alias HexSolver.{Assignment, PackageRange, PartialSolution, Term}
defstruct assignments: [],
decisions: %{},
positive: %{},
negative: %{},
backtracking: false,
attempted_solutions: 1
def relation(%PartialSolution{} = solution, %Term{} = term, opts \\ []) do
name = term.package_range.name
case Map.fetch(solution.positive, name) do
{:ok, positive} ->
Term.relation(positive.term, term, opts)
:error ->
case Map.fetch(solution.negative, name) do
{:ok, negative} -> Term.relation(negative.term, term, opts)
:error -> :overlapping
end
end
end
def satisfies?(%PartialSolution{} = solution, %Term{} = term, opts \\ []) do
relation(solution, term, opts) == :subset
end
def satisfier(%PartialSolution{} = solution, term, opts \\ []) do
{:satisfier, assignment} =
Enum.reduce_while(Enum.reverse(solution.assignments), nil, fn assignment, assigned_term ->
if assignment.term.package_range.name == term.package_range.name do
if Term.compatible_package?(assignment.term, term) do
assigned_term =
if assigned_term do
Assignment.intersect(assigned_term, assignment)
else
assignment
end
if Term.satisfies?(assigned_term.term, term, opts) do
{:halt, {:satisfier, assignment}}
else
{:cont, assigned_term}
end
else
if assignment.term.positive do
false = term.positive
{:halt, {:satisfier, assignment}}
else
{:cont, assigned_term}
end
end
else
{:cont, assigned_term}
end
end)
assignment
end
def backtrack(%PartialSolution{} = solution, decision_level) do
{removed, assignments} =
Enum.split_while(solution.assignments, &(&1.decision_level > decision_level))
removed_names = Enum.map(removed, & &1.term.package_range.name)
removed_decisions = Enum.filter(removed, &Assignment.decision?/1)
decisions =
Map.drop(solution.decisions, Enum.map(removed_decisions, & &1.term.package_range.name))
positive = Map.drop(solution.positive, removed_names)
negative = Map.drop(solution.negative, removed_names)
solution = %{
solution
| assignments: assignments,
decisions: decisions,
positive: positive,
negative: negative,
backtracking: true
}
Enum.reduce(assignments, solution, fn assignment, solution ->
if assignment.term.package_range.name in removed_names do
register(solution, assignment)
else
solution
end
end)
end
def derive(%PartialSolution{} = solution, term, incompatibility) do
{assignment, solution} = assign(solution, term, incompatibility)
register(solution, assignment)
end
def decide(
%PartialSolution{} = solution,
%PackageRange{repo: repo, name: package, constraint: version} = package_range
) do
attempted_solutions = solution.attempted_solutions + if solution.backtracking, do: 1, else: 0
decisions = Map.put(solution.decisions, package, {version, repo})
term = %Term{package_range: package_range, positive: true}
solution = %{
solution
| attempted_solutions: attempted_solutions,
backtracking: false,
decisions: decisions
}
{assignment, solution} = assign(solution, term, nil)
register(solution, assignment)
end
defp assign(solution, term, incompatibility) do
assignment = %Assignment{
term: term,
decision_level: map_size(solution.decisions),
index: length(solution.assignments),
cause: incompatibility
}
solution = %{solution | assignments: [assignment | solution.assignments]}
{assignment, solution}
end
defp register(solution, assignment) do
name = assignment.term.package_range.name
case Map.fetch(solution.positive, name) do
{:ok, old_assignment} ->
assignment = Assignment.intersect(old_assignment, assignment)
positive = Map.put(solution.positive, name, assignment)
%{solution | positive: positive}
:error ->
assignment =
if old_assignment = Map.get(solution.negative, name) do
Assignment.intersect(old_assignment, assignment)
else
assignment
end
if assignment.term.positive do
negative = Map.delete(solution.negative, name)
positive = Map.put(solution.positive, name, assignment)
%{solution | negative: negative, positive: positive}
else
negative = Map.put(solution.negative, name, assignment)
%{solution | negative: negative}
end
end
end
def unsatisfied(%PartialSolution{} = solution) do
solution.positive
|> Map.values()
|> Enum.reject(&Map.has_key?(solution.decisions, &1.term.package_range.name))
|> Enum.reject(& &1.term.optional)
|> Enum.map(& &1.term.package_range)
end
end