Skip to content

Commit 8923a46

Browse files
committed
Fix space leak in solver backjumping
This commit refactors backjumping so that it uses the 'Progress' type instead of separate references to a node's children and the conflict set calculated from those children.
1 parent 37f28f2 commit 8923a46

File tree

3 files changed

+60
-86
lines changed

3 files changed

+60
-86
lines changed
Lines changed: 50 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
module Distribution.Client.Dependency.Modular.Explore
22
( backjump
3-
, exploreTreeLog
3+
, backjumpAndExplore
44
) where
55

6-
import Control.Applicative as A
7-
import Data.Foldable
8-
import Data.List as L
6+
import Data.Foldable as F
97
import Data.Map as M
108
import Data.Set as S
119

@@ -16,107 +14,81 @@ import Distribution.Client.Dependency.Modular.Message
1614
import Distribution.Client.Dependency.Modular.Package
1715
import qualified Distribution.Client.Dependency.Modular.PSQ as P
1816
import Distribution.Client.Dependency.Modular.Tree
17+
import qualified Distribution.Client.Dependency.Types as T
1918

20-
-- | Backjumping.
19+
-- | This function takes the variable we're currently considering and a
20+
-- list of children's logs. Each log yields either a solution or a
21+
-- conflict set. The result is a combined log for the parent node that
22+
-- has explored a prefix of the children.
2123
--
22-
-- A tree traversal that tries to propagate conflict sets
23-
-- up the tree from the leaves, and thereby cut branches.
24-
-- All the tricky things are done in the function 'combine'.
25-
backjump :: Tree a -> Tree (Maybe (ConflictSet QPN))
26-
backjump = snd . cata go
27-
where
28-
go (FailF c fr) = (Just c, Fail c fr)
29-
go (DoneF rdm ) = (Nothing, Done rdm)
30-
go (PChoiceF qpn _ ts) = (c, PChoice qpn c (P.fromList ts'))
31-
where
32-
~(c, ts') = combine (P qpn) (P.toList ts) S.empty
33-
go (FChoiceF qfn _ b m ts) = (c, FChoice qfn c b m (P.fromList ts'))
34-
where
35-
~(c, ts') = combine (F qfn) (P.toList ts) S.empty
36-
go (SChoiceF qsn _ b ts) = (c, SChoice qsn c b (P.fromList ts'))
37-
where
38-
~(c, ts') = combine (S qsn) (P.toList ts) S.empty
39-
go (GoalChoiceF ts) = (c, GoalChoice (P.fromList ts'))
40-
where
41-
~(cs, ts') = unzip $ L.map (\ (k, (x, v)) -> (x, (k, v))) $ P.toList ts
42-
c = case cs of [] -> Nothing
43-
d : _ -> d
44-
45-
-- | The 'combine' function is at the heart of backjumping. It takes
46-
-- the variable we're currently considering, and a list of children
47-
-- annotated with their respective conflict sets, and an accumulator
48-
-- for the result conflict set. It returns a combined conflict set
49-
-- for the parent node, and a (potentially shortened) list of children
50-
-- with the annotations removed.
51-
--
52-
-- It is *essential* that we produce the results as early as possible.
53-
-- In particular, we have to produce the list of children prior to
54-
-- traversing the entire list -- otherwise we lose the desired behaviour
55-
-- of being able to traverse the tree from left to right incrementally.
56-
--
57-
-- We can shorten the list of children if we find an individual conflict
58-
-- set that does not contain the current variable. In this case, we can
59-
-- just lift the conflict set to the current level, because the current
60-
-- level cannot possibly have contributed to this conflict, so no other
61-
-- choice at the current level would avoid the conflict.
24+
-- We can stop traversing the children's logs if we find an individual
25+
-- conflict set that does not contain the current variable. In this
26+
-- case, we can just lift the conflict set to the current level,
27+
-- because the current level cannot possibly have contributed to this
28+
-- conflict, so no other choice at the current level would avoid the
29+
-- conflict.
6230
--
63-
-- If any of the children might contain a successful solution
64-
-- (indicated by Nothing), then Nothing will be the combined
65-
-- conflict set. If all children contain conflict sets, we can
31+
-- If any of the children might contain a successful solution, we can
32+
-- return it immediately. If all children contain conflict sets, we can
6633
-- take the union as the combined conflict set.
67-
combine :: Var QPN -> [(a, (Maybe (ConflictSet QPN), b))] ->
68-
ConflictSet QPN -> (Maybe (ConflictSet QPN), [(a, b)])
69-
combine _ [] c = (Just c, [])
70-
combine var ((k, ( d, v)) : xs) c = (\ ~(e, ys) -> (e, (k, v) : ys)) $
71-
case d of
72-
Just e | not (simplifyVar var `S.member` e) -> (Just e, [])
73-
| otherwise -> combine var xs (e `S.union` c)
74-
Nothing -> (Nothing, snd $ combine var xs S.empty)
34+
backjump :: F.Foldable t => Var QPN -> t (ConflictSetLog a) -> ConflictSetLog a
35+
backjump var xs = F.foldr combine backjumpInfo xs S.empty
36+
where
37+
combine :: ConflictSetLog a
38+
-> (ConflictSet QPN -> ConflictSetLog a)
39+
-> ConflictSet QPN -> ConflictSetLog a
40+
combine (T.Done x) _ _ = T.Done x
41+
combine (T.Fail cs) f csAcc
42+
| not (simplifyVar var `S.member` cs) = backjumpInfo cs
43+
| otherwise = f (csAcc `S.union` cs)
44+
combine (T.Step m ms) f cs = T.Step m (combine ms f cs)
45+
46+
type ConflictSetLog = T.Progress Message (ConflictSet QPN)
7547

76-
-- | Version of 'explore' that returns a 'Log'.
77-
exploreLog :: Tree (Maybe (ConflictSet QPN)) ->
78-
(Assignment -> Log Message (Assignment, RevDepMap))
48+
-- | A tree traversal that simultaneously propagates conflict sets up
49+
-- the tree from the leaves and creates a log.
50+
exploreLog :: Tree a -> (Assignment -> ConflictSetLog (Assignment, RevDepMap))
7951
exploreLog = cata go
8052
where
81-
go (FailF c fr) _ = failWith (Failure c fr)
53+
go :: TreeF a (Assignment -> ConflictSetLog (Assignment, RevDepMap))
54+
-> (Assignment -> ConflictSetLog (Assignment, RevDepMap))
55+
go (FailF c fr) _ = failWith (Failure c fr) c
8256
go (DoneF rdm) a = succeedWith Success (a, rdm)
83-
go (PChoiceF qpn c ts) (A pa fa sa) =
84-
backjumpInfo c $
85-
asum $ -- try children in order,
57+
go (PChoiceF qpn _ ts) (A pa fa sa) =
58+
backjump (P qpn) $ -- try children in order,
8659
P.mapWithKey -- when descending ...
87-
(\ i@(POption k _) r -> tryWith (TryP qpn i) $ -- log and ...
60+
(\ i@(POption k _) r -> tryWith (TryP qpn i) $ -- log and ...
8861
r (A (M.insert qpn k pa) fa sa)) -- record the pkg choice
8962
ts
90-
go (FChoiceF qfn c _ _ ts) (A pa fa sa) =
91-
backjumpInfo c $
92-
asum $ -- try children in order,
63+
go (FChoiceF qfn _ _ _ ts) (A pa fa sa) =
64+
backjump (F qfn) $ -- try children in order,
9365
P.mapWithKey -- when descending ...
9466
(\ k r -> tryWith (TryF qfn k) $ -- log and ...
9567
r (A pa (M.insert qfn k fa) sa)) -- record the pkg choice
9668
ts
97-
go (SChoiceF qsn c _ ts) (A pa fa sa) =
98-
backjumpInfo c $
99-
asum $ -- try children in order,
69+
go (SChoiceF qsn _ _ ts) (A pa fa sa) =
70+
backjump (S qsn) $ -- try children in order,
10071
P.mapWithKey -- when descending ...
10172
(\ k r -> tryWith (TryS qsn k) $ -- log and ...
10273
r (A pa fa (M.insert qsn k sa))) -- record the pkg choice
10374
ts
10475
go (GoalChoiceF ts) a =
10576
P.casePSQ ts
106-
(failWith (Failure S.empty EmptyGoalChoice)) -- empty goal choice is an internal error
107-
(\ k v _xs -> continueWith (Next (close k)) (v a)) -- commit to the first goal choice
77+
(failWith (Failure S.empty EmptyGoalChoice) S.empty) -- empty goal choice is an internal error
78+
(\ k v _xs -> continueWith (Next (close k)) (v a)) -- commit to the first goal choice
10879

10980
-- | Add in information about pruned trees.
11081
--
11182
-- TODO: This isn't quite optimal, because we do not merely report the shape of the
11283
-- tree, but rather make assumptions about where that shape originated from. It'd be
11384
-- better if the pruning itself would leave information that we could pick up at this
11485
-- point.
115-
backjumpInfo :: Maybe (ConflictSet QPN) -> Log Message a -> Log Message a
116-
backjumpInfo c m = m <|> case c of -- important to produce 'm' before matching on 'c'!
117-
Nothing -> A.empty
118-
Just cs -> failWith (Failure cs Backjump)
86+
backjumpInfo :: ConflictSet QPN -> ConflictSetLog a
87+
backjumpInfo cs = failWith (Failure cs Backjump) cs
11988

12089
-- | Interface.
121-
exploreTreeLog :: Tree (Maybe (ConflictSet QPN)) -> Log Message (Assignment, RevDepMap)
122-
exploreTreeLog t = exploreLog t (A M.empty M.empty M.empty)
90+
backjumpAndExplore :: Tree a -> Log Message (Assignment, RevDepMap)
91+
backjumpAndExplore t = toLog $ exploreLog t (A M.empty M.empty M.empty)
92+
where
93+
toLog :: T.Progress step fail done -> Log step done
94+
toLog = T.foldProgress T.Step (const (T.Fail ())) T.Done

cabal-install/Distribution/Client/Dependency/Modular/Log.hs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@ logToProgress mbj l = let
9191
go _ _ (Done s) = Done s
9292
go _ _ (Fail (_, Nothing)) = Fail ("Could not resolve dependencies; something strange happened.") -- should not happen
9393

94-
failWith :: m -> Log m a
95-
failWith m = Step m (Fail ())
94+
failWith :: step -> fail -> Progress step fail done
95+
failWith s f = Step s (Fail f)
9696

97-
succeedWith :: m -> a -> Log m a
98-
succeedWith m x = Step m (Done x)
97+
succeedWith :: step -> done -> Progress step fail done
98+
succeedWith s d = Step s (Done d)
9999

100-
continueWith :: m -> Log m a -> Log m a
100+
continueWith :: step -> Progress step fail done -> Progress step fail done
101101
continueWith = Step
102102

103-
tryWith :: Message -> Log Message a -> Log Message a
104-
tryWith m x = Step m (Step Enter x) <|> failWith Leave
103+
tryWith :: Message
104+
-> Progress Message fail done
105+
-> Progress Message fail done
106+
tryWith m = Step m . Step Enter . foldProgress Step (failWith Leave) Done

cabal-install/Distribution/Client/Dependency/Modular/Solver.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ solve sc cinfo idx userPrefs userConstraints userGoals =
4646
prunePhase $
4747
buildPhase
4848
where
49-
explorePhase = exploreTreeLog . backjump
49+
explorePhase = backjumpAndExplore
5050
heuristicsPhase = P.firstGoal . -- after doing goal-choice heuristics, commit to the first choice (saves space)
5151
P.deferSetupChoices .
5252
P.deferWeakFlagChoices .

0 commit comments

Comments
 (0)