From a5ad9c1ea7acd4ef6c8accd4188aba830f5983d3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 4 Nov 2020 19:40:21 +0800 Subject: [PATCH] Mark downstream incompatibilities on backtracking 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. --- src/resolvelib/resolvers.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 976608b..8152595 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -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, @@ -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