Merge pull request #10546 from cabalism/fix/dedup-using-config-from
[cabal.git] / cabal-install-solver / src / Distribution / Solver / Modular / Explore.hs
blob90038a28f5c198e4e82909b08989dbb334f129be
1 {-# LANGUAGE BangPatterns #-}
2 {-# LANGUAGE LambdaCase #-}
3 {-# LANGUAGE ScopedTypeVariables #-}
4 module Distribution.Solver.Modular.Explore (backjumpAndExplore) where
6 import Distribution.Solver.Compat.Prelude
7 import Prelude ()
9 import qualified Distribution.Solver.Types.Progress as P
11 import qualified Data.List as L (foldl')
12 import qualified Data.Map.Strict as M
13 import qualified Data.Set as S
15 import Distribution.Simple.Setup (asBool)
17 import Distribution.Solver.Modular.Assignment
18 import Distribution.Solver.Modular.Dependency
19 import Distribution.Solver.Modular.Index
20 import Distribution.Solver.Modular.Log
21 import Distribution.Solver.Modular.Message
22 import Distribution.Solver.Modular.Package
23 import qualified Distribution.Solver.Modular.PSQ as P
24 import qualified Distribution.Solver.Modular.ConflictSet as CS
25 import Distribution.Solver.Modular.RetryLog
26 import Distribution.Solver.Modular.Tree
27 import Distribution.Solver.Modular.Version
28 import qualified Distribution.Solver.Modular.WeightedPSQ as W
29 import Distribution.Solver.Types.PackagePath
30 import Distribution.Solver.Types.Settings
31 (CountConflicts(..), EnableBackjumping(..), FineGrainedConflicts(..))
32 import Distribution.Types.VersionRange (anyVersion)
34 -- | This function takes the variable we're currently considering, a
35 -- last conflict set and a list of children's logs. Each log yields
36 -- either a solution or a conflict set. The result is a combined log for
37 -- the parent node that has explored a prefix of the children.
39 -- We can stop traversing the children's logs if we find an individual
40 -- conflict set that does not contain the current variable. In this
41 -- case, we can just lift the conflict set to the current level,
42 -- because the current level cannot possibly have contributed to this
43 -- conflict, so no other choice at the current level would avoid the
44 -- conflict.
46 -- If any of the children might contain a successful solution, we can
47 -- return it immediately. If all children contain conflict sets, we can
48 -- take the union as the combined conflict set.
50 -- The last conflict set corresponds to the justification that we
51 -- have to choose this goal at all. There is a reason why we have
52 -- introduced the goal in the first place, and this reason is in conflict
53 -- with the (virtual) option not to choose anything for the current
54 -- variable. See also the comments for 'avoidSet'.
56 -- We can also skip a child if it does not resolve any of the conflicts paired
57 -- with the current variable in the previous child's conflict set. 'backjump'
58 -- takes a function to determine whether a child can be skipped. If the child
59 -- can be skipped, the function returns a new conflict set to be merged with the
60 -- previous conflict set.
62 backjump :: forall w k a . Maybe Int
63 -> EnableBackjumping
64 -> FineGrainedConflicts
66 -> (k -> S.Set CS.Conflict -> Maybe ConflictSet)
67 -- ^ Function that determines whether the given choice could resolve
68 -- the given conflict. It indicates false by returning 'Just',
69 -- with the new conflicts to be added to the conflict set.
71 -> (k -> ConflictSet -> ExploreState -> ConflictSetLog a)
72 -- ^ Function that logs the given choice that was skipped.
74 -> Var QPN -- ^ The current variable.
76 -> ConflictSet -- ^ Conflict set representing the reason that the goal
77 -- was introduced.
79 -> W.WeightedPSQ w k (ExploreState -> ConflictSetLog a)
80 -- ^ List of children's logs.
82 -> ExploreState -> ConflictSetLog a
83 backjump mbj enableBj fineGrainedConflicts couldResolveConflicts
84 logSkippedChoice var lastCS xs =
85 foldr combine avoidGoal [(k, v) | (_, k, v) <- W.toList xs] CS.empty Nothing
86 where
87 combine :: (k, ExploreState -> ConflictSetLog a)
88 -> (ConflictSet -> Maybe ConflictSet -> ExploreState -> ConflictSetLog a)
89 -> ConflictSet -> Maybe ConflictSet -> ExploreState -> ConflictSetLog a
90 combine (k, x) f csAcc mPreviousCS es =
91 case (asBool fineGrainedConflicts, mPreviousCS) of
92 (True, Just previousCS) ->
93 case CS.lookup var previousCS of
94 Just conflicts ->
95 case couldResolveConflicts k conflicts of
96 Nothing -> retryNoSolution (x es) next
97 Just newConflicts -> skipChoice (previousCS `CS.union` newConflicts)
98 _ -> skipChoice previousCS
99 _ -> retryNoSolution (x es) next
100 where
101 next :: ConflictSet -> ExploreState -> ConflictSetLog a
102 next !cs es' = if asBool enableBj && not (var `CS.member` cs)
103 then skipLoggingBackjump cs es'
104 else f (csAcc `CS.union` cs) (Just cs) es'
106 -- This function is for skipping the choice when it cannot resolve any
107 -- of the previous conflicts.
108 skipChoice :: ConflictSet -> ConflictSetLog a
109 skipChoice newCS =
110 retryNoSolution (logSkippedChoice k newCS es) $ \cs' es' ->
111 f (csAcc `CS.union` cs') (Just cs') $
113 -- Update the conflict map with the conflict set, to make up for
114 -- skipping the whole subtree.
115 es' { esConflictMap = updateCM cs' (esConflictMap es') }
117 -- This function represents the option to not choose a value for this goal.
118 avoidGoal :: ConflictSet -> Maybe ConflictSet -> ExploreState -> ConflictSetLog a
119 avoidGoal cs _mPreviousCS !es =
120 logBackjump mbj (cs `CS.union` lastCS) $
122 -- Use 'lastCS' below instead of 'cs' since we do not want to
123 -- double-count the additionally accumulated conflicts.
124 es { esConflictMap = updateCM lastCS (esConflictMap es) }
126 -- The solver does not count or log backjumps at levels where the conflict
127 -- set does not contain the current variable. Otherwise, there would be many
128 -- consecutive log messages about backjumping with the same conflict set.
129 skipLoggingBackjump :: ConflictSet -> ExploreState -> ConflictSetLog a
130 skipLoggingBackjump cs es = fromProgress $ P.Fail (NoSolution cs es)
132 -- | Creates a failing ConflictSetLog representing a backjump. It inserts a
133 -- "backjumping" message, checks whether the backjump limit has been reached,
134 -- and increments the backjump count.
135 logBackjump :: Maybe Int -> ConflictSet -> ExploreState -> ConflictSetLog a
136 logBackjump mbj cs es =
137 failWith (Failure cs Backjump) $
138 if reachedBjLimit (esBackjumps es)
139 then BackjumpLimit
140 else NoSolution cs es { esBackjumps = esBackjumps es + 1 }
141 where
142 reachedBjLimit = case mbj of
143 Nothing -> const False
144 Just limit -> (>= limit)
146 -- | Like 'retry', except that it only applies the input function when the
147 -- backjump limit has not been reached.
148 retryNoSolution :: ConflictSetLog a
149 -> (ConflictSet -> ExploreState -> ConflictSetLog a)
150 -> ConflictSetLog a
151 retryNoSolution lg f = retry lg $ \case
152 BackjumpLimit -> fromProgress (P.Fail BackjumpLimit)
153 NoSolution cs es -> f cs es
155 -- | The state that is read and written while exploring the search tree.
156 data ExploreState = ES {
157 esConflictMap :: !ConflictMap
158 , esBackjumps :: !Int
161 data IntermediateFailure =
162 NoSolution ConflictSet ExploreState
163 | BackjumpLimit
165 type ConflictSetLog = RetryLog Message IntermediateFailure
167 getBestGoal :: ConflictMap -> P.PSQ (Goal QPN) a -> (Goal QPN, a)
168 getBestGoal cm =
169 P.maximumBy
170 ( flip (M.findWithDefault 0) cm
171 . (\ (Goal v _) -> v)
174 getFirstGoal :: P.PSQ (Goal QPN) a -> (Goal QPN, a)
175 getFirstGoal ts =
176 P.casePSQ ts
177 (error "getFirstGoal: empty goal choice") -- empty goal choice is an internal error
178 (\ k v _xs -> (k, v)) -- commit to the first goal choice
180 updateCM :: ConflictSet -> ConflictMap -> ConflictMap
181 updateCM cs cm =
182 L.foldl' (\ cmc k -> M.insertWith (+) k 1 cmc) cm (CS.toList cs)
184 -- | Record complete assignments on 'Done' nodes.
185 assign :: Tree d c -> Tree Assignment c
186 assign tree = go tree (A M.empty M.empty M.empty)
187 where
188 go :: Tree d c -> Assignment -> Tree Assignment c
189 go (Fail c fr) _ = Fail c fr
190 go (Done rdm _) a = Done rdm a
191 go (PChoice qpn rdm y ts) (A pa fa sa) = PChoice qpn rdm y $ W.mapWithKey f (fmap go ts)
192 where f (POption k _) r = r (A (M.insert qpn k pa) fa sa)
193 go (FChoice qfn rdm y t m d ts) (A pa fa sa) = FChoice qfn rdm y t m d $ W.mapWithKey f (fmap go ts)
194 where f k r = r (A pa (M.insert qfn k fa) sa)
195 go (SChoice qsn rdm y t ts) (A pa fa sa) = SChoice qsn rdm y t $ W.mapWithKey f (fmap go ts)
196 where f k r = r (A pa fa (M.insert qsn k sa))
197 go (GoalChoice rdm ts) a = GoalChoice rdm $ fmap ($ a) (fmap go ts)
199 -- | A tree traversal that simultaneously propagates conflict sets up
200 -- the tree from the leaves and creates a log.
201 exploreLog :: Maybe Int
202 -> EnableBackjumping
203 -> FineGrainedConflicts
204 -> CountConflicts
205 -> Index
206 -> Tree Assignment QGoalReason
207 -> ConflictSetLog (Assignment, RevDepMap)
208 exploreLog mbj enableBj fineGrainedConflicts (CountConflicts countConflicts) idx t =
209 para go t initES
210 where
211 getBestGoal' :: P.PSQ (Goal QPN) a -> ConflictMap -> (Goal QPN, a)
212 getBestGoal'
213 | asBool countConflicts = \ ts cm -> getBestGoal cm ts
214 | otherwise = \ ts _ -> getFirstGoal ts
216 go :: TreeF Assignment QGoalReason
217 (ExploreState -> ConflictSetLog (Assignment, RevDepMap), Tree Assignment QGoalReason)
218 -> (ExploreState -> ConflictSetLog (Assignment, RevDepMap))
219 go (FailF c fr) = \ !es ->
220 let es' = es { esConflictMap = updateCM c (esConflictMap es) }
221 in failWith (Failure c fr) (NoSolution c es')
222 go (DoneF rdm a) = \ _ -> succeedWith Success (a, rdm)
223 go (PChoiceF qpn _ gr ts) =
224 backjump mbj enableBj fineGrainedConflicts
225 (couldResolveConflicts qpn)
226 (logSkippedPackage qpn)
227 (P qpn) (avoidSet (P qpn) gr) $ -- try children in order,
228 W.mapWithKey -- when descending ...
229 (\ k r es -> tryWith (TryP qpn k) (r es))
230 (fmap fst ts)
231 go (FChoiceF qfn _ gr _ _ _ ts) =
232 backjump mbj enableBj fineGrainedConflicts
233 (\_ _ -> Nothing)
234 (const logSkippedChoiceSimple)
235 (F qfn) (avoidSet (F qfn) gr) $ -- try children in order,
236 W.mapWithKey -- when descending ...
237 (\ k r es -> tryWith (TryF qfn k) (r es))
238 (fmap fst ts)
239 go (SChoiceF qsn _ gr _ ts) =
240 backjump mbj enableBj fineGrainedConflicts
241 (\_ _ -> Nothing)
242 (const logSkippedChoiceSimple)
243 (S qsn) (avoidSet (S qsn) gr) $ -- try children in order,
244 W.mapWithKey -- when descending ...
245 (\ k r es -> tryWith (TryS qsn k) (r es))
246 (fmap fst ts)
247 go (GoalChoiceF _ ts) = \ es ->
248 let (k, (v, tree)) = getBestGoal' ts (esConflictMap es)
249 in continueWith (Next k) $
250 -- Goal choice nodes are normally not counted as backjumps, since the
251 -- solver always explores exactly one choice, which means that the
252 -- backjump from the goal choice would be redundant with the backjump
253 -- from the PChoice, FChoice, or SChoice below. The one case where the
254 -- backjump is not redundant is when the chosen goal is a failure node,
255 -- so we log a backjump in that case.
256 case tree of
257 Fail _ _ -> retryNoSolution (v es) $ logBackjump mbj
258 _ -> v es
260 initES = ES {
261 esConflictMap = M.empty
262 , esBackjumps = 0
265 -- Is it possible for this package instance (QPN and POption) to resolve any
266 -- of the conflicts that were caused by the previous instance? The default
267 -- is true, because it is always safe to explore a package instance.
268 -- Skipping it is an optimization. If false, it returns a new conflict set
269 -- to be merged with the previous one.
270 couldResolveConflicts :: QPN -> POption -> S.Set CS.Conflict -> Maybe ConflictSet
271 couldResolveConflicts currentQPN@(Q _ pn) (POption i@(I v _) _) conflicts =
272 let (PInfo deps _ _ _) = idx M.! pn M.! i
273 qdeps = qualifyDeps (defaultQualifyOptions idx) currentQPN deps
275 couldBeResolved :: CS.Conflict -> Maybe ConflictSet
276 couldBeResolved CS.OtherConflict = Nothing
277 couldBeResolved (CS.GoalConflict conflictingDep) =
278 -- Check whether this package instance also has 'conflictingDep'
279 -- as a dependency (ignoring flag and stanza choices).
280 if null [() | Simple (LDep _ (Dep (PkgComponent qpn _) _)) _ <- qdeps, qpn == conflictingDep]
281 then Nothing
282 else Just CS.empty
283 couldBeResolved (CS.VersionConstraintConflict dep excludedVersion) =
284 -- Check whether this package instance also excludes version
285 -- 'excludedVersion' of 'dep' (ignoring flag and stanza choices).
286 let vrs = [vr | Simple (LDep _ (Dep (PkgComponent qpn _) (Constrained vr))) _ <- qdeps, qpn == dep ]
287 vrIntersection = L.foldl' (.&&.) anyVersion vrs
288 in if checkVR vrIntersection excludedVersion
289 then Nothing
290 else -- If we skip this package instance, we need to update the
291 -- conflict set to say that 'dep' was also excluded by
292 -- this package instance's constraint.
293 Just $ CS.singletonWithConflict (P dep) $
294 CS.VersionConflict currentQPN (CS.OrderedVersionRange vrIntersection)
295 couldBeResolved (CS.VersionConflict reverseDep (CS.OrderedVersionRange excludingVR)) =
296 -- Check whether this package instance's version is also excluded
297 -- by 'excludingVR'.
298 if checkVR excludingVR v
299 then Nothing
300 else -- If we skip this version, we need to update the conflict
301 -- set to say that the reverse dependency also excluded this
302 -- version.
303 Just $ CS.singletonWithConflict (P reverseDep) (CS.VersionConstraintConflict currentQPN v)
304 in fmap CS.unions $ traverse couldBeResolved (S.toList conflicts)
306 logSkippedPackage :: QPN -> POption -> ConflictSet -> ExploreState -> ConflictSetLog a
307 logSkippedPackage qpn pOption cs es =
308 tryWith (TryP qpn pOption) $
309 failWith (Skip (fromMaybe S.empty $ CS.lookup (P qpn) cs)) $
310 NoSolution cs es
312 -- This function is used for flag and stanza choices, but it should not be
313 -- called, because there is currently no way to skip a value for a flag or
314 -- stanza.
315 logSkippedChoiceSimple :: ConflictSet -> ExploreState -> ConflictSetLog a
316 logSkippedChoiceSimple cs es = fromProgress $ P.Fail $ NoSolution cs es
318 -- | Build a conflict set corresponding to the (virtual) option not to
319 -- choose a solution for a goal at all.
321 -- In the solver, the set of goals is not statically determined, but depends
322 -- on the choices we make. Therefore, when dealing with conflict sets, we
323 -- always have to consider that we could perhaps make choices that would
324 -- avoid the existence of the goal completely.
326 -- Whenever we actually introduce a choice in the tree, we have already established
327 -- that the goal cannot be avoided. This is tracked in the "goal reason".
328 -- The choice to avoid the goal therefore is a conflict between the goal itself
329 -- and its goal reason. We build this set here, and pass it to the 'backjump'
330 -- function as the last conflict set.
332 -- This has two effects:
334 -- - In a situation where there are no choices available at all (this happens
335 -- if an unknown package is requested), the last conflict set becomes the
336 -- actual conflict set.
338 -- - In a situation where all of the children's conflict sets contain the
339 -- current variable, the goal reason of the current node will be added to the
340 -- conflict set.
342 avoidSet :: Var QPN -> QGoalReason -> ConflictSet
343 avoidSet var@(P qpn) gr =
344 CS.union (CS.singleton var) (goalReasonToConflictSetWithConflict qpn gr)
345 avoidSet var gr =
346 CS.union (CS.singleton var) (goalReasonToConflictSet gr)
348 -- | Interface.
350 -- Takes as an argument a limit on allowed backjumps. If the limit is 'Nothing',
351 -- then infinitely many backjumps are allowed. If the limit is 'Just 0',
352 -- backtracking is completely disabled.
353 backjumpAndExplore :: Maybe Int
354 -> EnableBackjumping
355 -> FineGrainedConflicts
356 -> CountConflicts
357 -> Index
358 -> Tree d QGoalReason
359 -> RetryLog Message SolverFailure (Assignment, RevDepMap)
360 backjumpAndExplore mbj enableBj fineGrainedConflicts countConflicts idx =
361 mapFailure convertFailure
362 . exploreLog mbj enableBj fineGrainedConflicts countConflicts idx
363 . assign
364 where
365 convertFailure (NoSolution cs es) = ExhaustiveSearch cs (esConflictMap es)
366 convertFailure BackjumpLimit = BackjumpLimitReached