Skip to content

Commit

Permalink
Mark downstream incompatibilities on backtracking
Browse files Browse the repository at this point in the history
Consider this situation:

* Candidate Ax depends on B
* Candidate B1 depends on C==1, D==1, E==1
* Candidate B2 depends on C==2, D==1, E==1
* Candidate C1 depends on D==1
* Candidate C2 depends on D==1
* Candidate D1 depends on E!=1

In the previous implementation, the conflict on E is discovered after we
resolved Ax-B1-C1-D1. D1 is marked as an incompatibility, we backtrack
the C1 pin. But now we don't have an available C, and need to also
backtrack the B1 pin. At this point, however, the previous implemen-
tation would fail to "remember" that D1 is also marked as incompatible,
and proceed to try B2. That would eventually fail, we backtrack to the
same point, are stuck trying B1 and B2 repeatedly.

This fix uses a list to remember all the candidates marked as
incompatible along the whole backtrack process, and "re-mark" them in
parent states. This makes the resolver aware, when it backtracks B1,
that B2 is also not viable, and avoid hitting it.
  • Loading branch information
uranusjr committed Nov 4, 2020
1 parent 1a302b6 commit a5ad9c1
Showing 1 changed file with 20 additions and 8 deletions.
28 changes: 20 additions & 8 deletions src/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,25 @@ def _attempt_to_pin_criterion(self, name, criterion):
# end, signal for backtracking.
return causes

def _mark_backtrack_on_state(self, incompatibilities):
state = self.state
for name, candidates in incompatibilities.items():
try:
criterion = state.criteria[name]
except KeyError:
continue
criterion = criterion.excluded_of(candidates)
if criterion is None:
return False
state.criteria[name] = criterion
return True

def _backtrack(self):
# Drop the current state, it's known not to work.
del self._states[-1]

incompatibilities = collections.defaultdict(list)

# We need at least 2 states here:
# (a) One to backtrack to.
# (b) One to restore state (a) to its state prior to candidate-pinning,
Expand All @@ -255,19 +270,16 @@ def _backtrack(self):
except KeyError:
continue
self._r.backtracking(candidate)
incompatibilities[name].append(candidate)

# Mark candidates identified during backtracking as incompatible.
if not self._mark_backtrack_on_state(incompatibilities):
continue

# Create a new state to work on, with the newly known not-working
# candidate excluded.
self._push_new_state()

# Mark the retracted candidate as incompatible.
criterion = self.state.criteria[name].excluded_of(candidate)
if criterion is None:
# This state still does not work. Try the still previous state.
del self._states[-1]
continue
self.state.criteria[name] = criterion

return True

return False
Expand Down

0 comments on commit a5ad9c1

Please sign in to comment.