From 8c13253bbe275e19c019c96336f78d9c964b0092 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sun, 11 Nov 2018 20:43:03 -0800 Subject: [PATCH 01/10] Refactor generation of the solver's error message. This commit generates the solver's final error message in a separate function, which is more flexible when the solver is run multiple times. --- cabal-install/Distribution/Solver/Modular.hs | 44 ++++++++++++++----- .../Distribution/Solver/Modular/Log.hs | 37 +++------------- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 157bd921643..28023572128 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -116,32 +116,39 @@ solve' :: SolverConfig -> Set PN -> Progress String String (Assignment, RevDepMap) solve' sc cinfo idx pkgConfigDB pprefs gcs pns = - foldProgress Step (uncurry createErrorMsg) Done (runSolver printFullLog sc) + foldProgress Step + (createErrorMsg (solverVerbosity sc) (maxBackjumps sc)) + Done + (runSolver printFullLog sc) where runSolver :: Bool -> SolverConfig - -> Progress String (SolverFailure, String) (Assignment, RevDepMap) + -> Progress String SolverFailure (Assignment, RevDepMap) runSolver keepLog sc' = - logToProgress keepLog (solverVerbosity sc') (maxBackjumps sc') $ + logToProgress keepLog $ solve sc' cinfo idx pkgConfigDB pprefs gcs pns - createErrorMsg :: SolverFailure -> String + createErrorMsg :: Verbosity + -> Maybe Int + -> SolverFailure -> Progress String String (Assignment, RevDepMap) - createErrorMsg (ExhaustiveSearch cs _) msg = - Fail $ rerunSolverForErrorMsg cs ++ msg - createErrorMsg BackjumpLimitReached msg = + createErrorMsg verbosity mbj failure@(ExhaustiveSearch cs _) = + Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure + createErrorMsg verbosity mbj failure@BackjumpLimitReached = Step ("Backjump limit reached. Rerunning dependency solver to generate " ++ "a final conflict set for the search tree containing the " ++ "first backjump.") $ - foldProgress Step (f . fst) Done $ + foldProgress Step f Done $ runSolver printFullLog sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True } where f :: SolverFailure -> Progress String String (Assignment, RevDepMap) - f (ExhaustiveSearch cs _) = Fail $ rerunSolverForErrorMsg cs ++ msg + f (ExhaustiveSearch cs _) = + Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure f BackjumpLimitReached = -- This case is possible when the number of goals involved in -- conflicts is greater than the backjump limit. - Fail $ msg ++ "Failed to generate a summarized dependency solver " + Fail $ finalErrorMsg verbosity mbj failure + ++ "Failed to generate a summarized dependency solver " ++ "log due to low backjump limit." rerunSolverForErrorMsg :: ConflictSet -> String @@ -173,3 +180,20 @@ preferGoalsFromConflictSet cs = toVar (PackageVar qpn) = P qpn toVar (FlagVar qpn fn) = F (FN qpn fn) toVar (StanzaVar qpn sn) = S (SN qpn sn) + +finalErrorMsg :: Verbosity -> Maybe Int -> SolverFailure -> String +finalErrorMsg verbosity mbj failure = + case failure of + ExhaustiveSearch cs cm -> + "After searching the rest of the dependency tree exhaustively, " + ++ "these were the goals I've had most trouble fulfilling: " + ++ showCS cm cs + where + showCS = if verbosity > normal + then CS.showCSWithFrequency + else CS.showCSSortedByFrequency + BackjumpLimitReached -> + "Backjump limit reached (" ++ currlimit mbj ++ + "change with --max-backjumps or try to run with --reorder-goals).\n" + where currlimit (Just n) = "currently " ++ show n ++ ", " + currlimit Nothing = "" diff --git a/cabal-install/Distribution/Solver/Modular/Log.hs b/cabal-install/Distribution/Solver/Modular/Log.hs index fa08137c6f0..2561e518ab0 100644 --- a/cabal-install/Distribution/Solver/Modular/Log.hs +++ b/cabal-install/Distribution/Solver/Modular/Log.hs @@ -10,47 +10,22 @@ import Distribution.Solver.Types.Progress import Distribution.Solver.Modular.Dependency import Distribution.Solver.Modular.Message -import qualified Distribution.Solver.Modular.ConflictSet as CS import Distribution.Solver.Modular.RetryLog -import Distribution.Verbosity -- | Information about a dependency solver failure. data SolverFailure = ExhaustiveSearch ConflictSet ConflictMap | BackjumpLimitReached --- | Postprocesses a log file. When the dependency solver fails to find a --- solution, the log ends with a SolverFailure and a message describing the --- failure. This function discards all log messages and avoids calling --- 'showMessages' if the log isn't needed (specified by 'keepLog'), for --- efficiency. +-- | Postprocesses a log file. This function discards all log messages and +-- avoids calling 'showMessages' if the log isn't needed (specified by +-- 'keepLog'), for efficiency. logToProgress :: Bool - -> Verbosity - -> Maybe Int -> RetryLog Message SolverFailure a - -> Progress String (SolverFailure, String) a -logToProgress keepLog verbosity mbj lg = + -> Progress String SolverFailure a +logToProgress keepLog lg = if keepLog then showMessages progress else foldProgress (const id) Fail Done progress where - progress = - -- Convert the RetryLog to a Progress (with toProgress) as late as - -- possible, to take advantage of efficient updates at failures. - toProgress $ - mapFailure (\failure -> (failure, finalErrorMsg failure)) lg - - finalErrorMsg :: SolverFailure -> String - finalErrorMsg (ExhaustiveSearch cs cm) = - "After searching the rest of the dependency tree exhaustively, " - ++ "these were the goals I've had most trouble fulfilling: " - ++ showCS cm cs - where - showCS = if verbosity > normal - then CS.showCSWithFrequency - else CS.showCSSortedByFrequency - finalErrorMsg BackjumpLimitReached = - "Backjump limit reached (" ++ currlimit mbj ++ - "change with --max-backjumps or try to run with --reorder-goals).\n" - where currlimit (Just n) = "currently " ++ show n ++ ", " - currlimit Nothing = "" + progress = toProgress lg From 5e317d189578414c6de15b00fd8e3ba201125e98 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sun, 11 Nov 2018 20:43:03 -0800 Subject: [PATCH 02/10] Use RetryLog when rerunning the solver. RetryLog is simpler and more efficient than Progress for continuing the solver log after an error. --- cabal-install/Distribution/Solver/Modular.hs | 47 +++++++++---------- .../Distribution/Solver/Modular/Log.hs | 10 ++-- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 28023572128..791189544bd 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE LambdaCase #-} + module Distribution.Solver.Modular ( modularResolver, SolverConfig(..), PruneAfterFirstSuccess(..) ) where @@ -30,9 +32,10 @@ import Distribution.Solver.Modular.Index import Distribution.Solver.Modular.IndexConversion ( convPIs ) import Distribution.Solver.Modular.Log - ( SolverFailure(..), logToProgress ) + ( SolverFailure(..), displayLogMessages ) import Distribution.Solver.Modular.Package ( PN ) +import Distribution.Solver.Modular.RetryLog import Distribution.Solver.Modular.Solver ( SolverConfig(..), PruneAfterFirstSuccess(..), solve ) import Distribution.Solver.Types.DependencyResolver @@ -116,40 +119,36 @@ solve' :: SolverConfig -> Set PN -> Progress String String (Assignment, RevDepMap) solve' sc cinfo idx pkgConfigDB pprefs gcs pns = - foldProgress Step - (createErrorMsg (solverVerbosity sc) (maxBackjumps sc)) - Done - (runSolver printFullLog sc) + toProgress $ retry (runSolver printFullLog sc) + (createErrorMsg (solverVerbosity sc) (maxBackjumps sc)) where runSolver :: Bool -> SolverConfig - -> Progress String SolverFailure (Assignment, RevDepMap) + -> RetryLog String SolverFailure (Assignment, RevDepMap) runSolver keepLog sc' = - logToProgress keepLog $ + displayLogMessages keepLog $ solve sc' cinfo idx pkgConfigDB pprefs gcs pns createErrorMsg :: Verbosity -> Maybe Int -> SolverFailure - -> Progress String String (Assignment, RevDepMap) + -> RetryLog String String (Assignment, RevDepMap) createErrorMsg verbosity mbj failure@(ExhaustiveSearch cs _) = - Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure + fromProgress $ Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure createErrorMsg verbosity mbj failure@BackjumpLimitReached = - Step ("Backjump limit reached. Rerunning dependency solver to generate " + continueWith ("Backjump limit reached. Rerunning dependency solver to generate " ++ "a final conflict set for the search tree containing the " ++ "first backjump.") $ - foldProgress Step f Done $ - runSolver printFullLog - sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True } - where - f :: SolverFailure -> Progress String String (Assignment, RevDepMap) - f (ExhaustiveSearch cs _) = - Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure - f BackjumpLimitReached = - -- This case is possible when the number of goals involved in - -- conflicts is greater than the backjump limit. - Fail $ finalErrorMsg verbosity mbj failure - ++ "Failed to generate a summarized dependency solver " - ++ "log due to low backjump limit." + retry (runSolver printFullLog sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True }) $ + \case + ExhaustiveSearch cs _ -> + fromProgress $ Fail $ + rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure + BackjumpLimitReached -> + -- This case is possible when the number of goals involved in + -- conflicts is greater than the backjump limit. + fromProgress $ Fail $ finalErrorMsg verbosity mbj failure + ++ "Failed to generate a summarized dependency solver " + ++ "log due to low backjump limit." rerunSolverForErrorMsg :: ConflictSet -> String rerunSolverForErrorMsg cs = @@ -162,7 +161,7 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = -- original goal order. goalOrder' = preferGoalsFromConflictSet cs <> fromMaybe mempty (goalOrder sc) - in unlines ("Could not resolve dependencies:" : messages (runSolver True sc')) + in unlines ("Could not resolve dependencies:" : messages (toProgress (runSolver True sc'))) printFullLog = solverVerbosity sc >= verbose diff --git a/cabal-install/Distribution/Solver/Modular/Log.hs b/cabal-install/Distribution/Solver/Modular/Log.hs index 2561e518ab0..321a051070b 100644 --- a/cabal-install/Distribution/Solver/Modular/Log.hs +++ b/cabal-install/Distribution/Solver/Modular/Log.hs @@ -1,5 +1,5 @@ module Distribution.Solver.Modular.Log - ( logToProgress + ( displayLogMessages , SolverFailure(..) ) where @@ -20,10 +20,10 @@ data SolverFailure = -- | Postprocesses a log file. This function discards all log messages and -- avoids calling 'showMessages' if the log isn't needed (specified by -- 'keepLog'), for efficiency. -logToProgress :: Bool - -> RetryLog Message SolverFailure a - -> Progress String SolverFailure a -logToProgress keepLog lg = +displayLogMessages :: Bool + -> RetryLog Message SolverFailure a + -> RetryLog String SolverFailure a +displayLogMessages keepLog lg = fromProgress $ if keepLog then showMessages progress else foldProgress (const id) Fail Done progress From 5a2bf8bf962d41b55ae5e0dd4deb59fc99c99559 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sun, 11 Nov 2018 20:43:03 -0800 Subject: [PATCH 03/10] Solver: Improve error message by finding a minimal conflict set (issue #5647). This commit improves the error message in the case where the solver finds no solution after an exhaustive search. Previously, cabal generated the error message by rerunning the solver and having it prefer goals from the final conflict set, so that the error message only mentioned conflicts between packages from the final conflict set. Conflicts relating to the final conflict set are more likely to be relevant, because the conflict set contains all variables that are involved in one conflict that makes the dependencies unsatisfiable. However, the conflict set can also include variables that led to some conflicts but aren't relevant to the main conflict. This commit improves the error message further by first trying to reduce the size of the conflict set. The current algorithm for reducing the conflict set simply reruns the solver with different goal orders, so it has some downsides: - Reducing the conflict set can be slow. It is also possible for the solver to reach the backjump limit on a rerun, even though the first run completed. In that case, it uses the original conflict set. - The function can fail to remove some unnecessary variables from the conflict set. In the worst case, it returns the original conflict set. I tested the feature on the example in https://www.reddit.com/r/haskell/comments/9rmh9s/how_to_read_cabal_solver_failure_output/ and #5647. Both runs used the command "cabal install --only-dependencies --enable-tests --force-reinstalls --index-state=2018-10-26T20:30:16Z". Before: Resolving dependencies... cabal: Could not resolve dependencies: [__0] trying: opaleye-0.6.7003.0 (user goal) [__1] rejecting: opaleye:!test (constraint from config file, command line flag, or user target requires opposite flag selection) [__1] trying: opaleye:*test [__2] trying: dotenv-0.6.0.3 (dependency of opaleye *test) [__3] trying: transformers-0.5.5.0/installed-0.5... (dependency of opaleye) [__4] next goal: contravariant (dependency of opaleye) [__4] rejecting: contravariant-1.5 (conflict: opaleye => contravariant>=1.2 && <1.5) [__4] rejecting: contravariant-1.4.1, contravariant-1.4 (conflict: transformers => base==4.12.0.0/installed-4.1..., contravariant => base<4.12) [__4] rejecting: contravariant-1.3.3, contravariant-1.3.2, contravariant-1.3.1.1, contravariant-1.3.1, contravariant-1.3, contravariant-1.2.2.1, contravariant-1.2.2, contravariant-1.2.1, contravariant-1.2.0.1, contravariant-1.2 (conflict: transformers==0.5.5.0/installed-0.5..., contravariant => transformers>=0.2 && <0.5) [__4] rejecting: contravariant-1.1, contravariant-1.0, contravariant-0.6.1.1, contravariant-0.6.1, contravariant-0.6, contravariant-0.5.2, contravariant-0.5.1, contravariant-0.5, contravariant-0.4.4, contravariant-0.4.3, contravariant-0.4.1, contravariant-0.4, contravariant-0.3, contravariant-0.2.0.2, contravariant-0.2.0.1, contravariant-0.2, contravariant-0.1.3, contravariant-0.1.2.1, contravariant-0.1.2, contravariant-0.1.1, contravariant-0.1.0.1, contravariant-0.1.0 (conflict: opaleye => contravariant>=1.2 && <1.5) [__4] fail (backjumping, conflict set: contravariant, opaleye, transformers) After searching the rest of the dependency tree exhaustively, these were the goals I've had most trouble fulfilling: transformers, contravariant, opaleye, base, dotenv, opaleye:test After: Resolving dependencies... cabal: Could not resolve dependencies: [__0] trying: opaleye-0.6.7003.0 (user goal) [__1] trying: transformers-0.5.5.0/installed-0.5... (dependency of opaleye) [__2] next goal: contravariant (dependency of opaleye) [__2] rejecting: contravariant-1.5 (conflict: opaleye => contravariant>=1.2 && <1.5) [__2] rejecting: contravariant-1.4.1, contravariant-1.4 (conflict: transformers => base==4.12.0.0/installed-4.1..., contravariant => base<4.12) [__2] rejecting: contravariant-1.3.3, contravariant-1.3.2, contravariant-1.3.1.1, contravariant-1.3.1, contravariant-1.3, contravariant-1.2.2.1, contravariant-1.2.2, contravariant-1.2.1, contravariant-1.2.0.1, contravariant-1.2 (conflict: transformers==0.5.5.0/installed-0.5..., contravariant => transformers>=0.2 && <0.5) [__2] rejecting: contravariant-1.1, contravariant-1.0, contravariant-0.6.1.1, contravariant-0.6.1, contravariant-0.6, contravariant-0.5.2, contravariant-0.5.1, contravariant-0.5, contravariant-0.4.4, contravariant-0.4.3, contravariant-0.4.1, contravariant-0.4, contravariant-0.3, contravariant-0.2.0.2, contravariant-0.2.0.1, contravariant-0.2, contravariant-0.1.3, contravariant-0.1.2.1, contravariant-0.1.2, contravariant-0.1.1, contravariant-0.1.0.1, contravariant-0.1.0 (conflict: opaleye => contravariant>=1.2 && <1.5) [__2] fail (backjumping, conflict set: contravariant, opaleye, transformers) After searching the rest of the dependency tree exhaustively, these were the goals I've had most trouble fulfilling: base, opaleye, contravariant, transformers In this case, the feature found a minimal conflict set, {base, opaleye, contravariant, transformers}, by removing dotenv and opaleye:test from the original conflict set. --- cabal-install/Distribution/Solver/Modular.hs | 149 ++++++++++++++++-- .../Solver/Modular/ConflictSet.hs | 10 ++ 2 files changed, 146 insertions(+), 13 deletions(-) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 791189544bd..4afc85e474b 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -1,4 +1,5 @@ {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ScopedTypeVariables #-} module Distribution.Solver.Modular ( modularResolver, SolverConfig(..), PruneAfterFirstSuccess(..) ) where @@ -15,7 +16,7 @@ import Prelude () import Distribution.Solver.Compat.Prelude import qualified Data.Map as M -import Data.Set (Set) +import Data.Set (Set, isSubsetOf) import Data.Ord import Distribution.Compat.Graph ( IsNode(..) ) @@ -132,10 +133,27 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = -> Maybe Int -> SolverFailure -> RetryLog String String (Assignment, RevDepMap) - createErrorMsg verbosity mbj failure@(ExhaustiveSearch cs _) = - fromProgress $ Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure + createErrorMsg verbosity mbj failure@(ExhaustiveSearch cs cm) = + continueWith ("Found no solution after exhaustively searching the " + ++ "dependency tree. Rerunning the dependency solver " + ++ "to minimize the conflict set ({" + ++ showConflictSet cs ++ "}).") $ + retry (tryToMinimizeConflictSet (runSolver printFullLog) sc cs cm) $ + \case + ExhaustiveSearch cs' cm' -> + fromProgress $ Fail $ + rerunSolverForErrorMsg cs' + ++ finalErrorMsg verbosity mbj (ExhaustiveSearch cs' cm') + BackjumpLimitReached -> + fromProgress $ Fail $ + "Reached backjump limit while trying to minimize the " + ++ "conflict set to create a better error message. " + ++ "Original error message:\n" + ++ rerunSolverForErrorMsg cs + ++ finalErrorMsg verbosity mbj failure createErrorMsg verbosity mbj failure@BackjumpLimitReached = - continueWith ("Backjump limit reached. Rerunning dependency solver to generate " + continueWith + ("Backjump limit reached. Rerunning dependency solver to generate " ++ "a final conflict set for the search tree containing the " ++ "first backjump.") $ retry (runSolver printFullLog sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True }) $ @@ -147,8 +165,8 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = -- This case is possible when the number of goals involved in -- conflicts is greater than the backjump limit. fromProgress $ Fail $ finalErrorMsg verbosity mbj failure - ++ "Failed to generate a summarized dependency solver " - ++ "log due to low backjump limit." + ++ "Failed to generate a summarized dependency solver " + ++ "log due to low backjump limit." rerunSolverForErrorMsg :: ConflictSet -> String rerunSolverForErrorMsg cs = @@ -168,17 +186,122 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = messages :: Progress step fail done -> [step] messages = foldProgress (:) (const []) (const []) +-- | Try to remove variables from the given conflict set to create a minimal +-- conflict set. +-- +-- Minimal means that no proper subset of the conflict set is also a conflict +-- set, though there may be other possible conflict sets with fewer variables. +-- This function minimizes the input by trying to remove one variable at a time. +-- It only makes one pass over the variables, so it runs the solver at most N +-- times when given a conflict set of size N. Only one pass is necessary, +-- because every superset of a conflict set is also a conflict set, meaning that +-- failing to remove variable X from a conflict set in one step means that X +-- cannot be removed from any subset of that conflict set in a subsequent step. +-- +-- Example steps: +-- +-- Start with {A, B, C}. +-- Try to remove A from {A, B, C} and fail. +-- Try to remove B from {A, B, C} and succeed. +-- Try to remove C from {A, C} and fail. +-- Return {A, C} +-- +-- This function can fail for two reasons: +-- +-- 1. The solver can reach the backjump limit on any run. In this case the +-- returned RetryLog ends with BackjumpLimitReached. +-- TODO: Consider applying the backjump limit to all solver runs combined, +-- instead of each individual run. For example, 10 runs with 10 backjumps +-- each should count as 100 backjumps. +-- 2. Since this function works by rerunning the solver, it is possible for the +-- solver to add new unnecessary variables to the conflict set. This function +-- discards the result from any run that adds new variables to the conflict +-- set, but the end result may not be completely minimized. +tryToMinimizeConflictSet :: forall a . (SolverConfig -> RetryLog String SolverFailure a) + -> SolverConfig + -> ConflictSet + -> ConflictMap + -> RetryLog String SolverFailure a +tryToMinimizeConflictSet runSolver sc cs cm = + foldl (\r v -> retryNoSolution r $ tryToRemoveOneVar v) + (fromProgress $ Fail $ ExhaustiveSearch cs cm) + (CS.toList cs) + where + -- This function runs the solver and makes it prefer goals in the following + -- order: + -- + -- 1. variables in 'smallestKnownCS', excluding 'v' + -- 2. 'v' + -- 3. all other variables + -- + -- If 'v' is not necessary, then the solver will find that there is no + -- solution before starting to solve for 'v', and the new final conflict set + -- will be very likely to not contain 'v'. If 'v' is necessary, the solver + -- will most likely need to try solving for 'v' before finding that there is + -- no solution, and the new final conflict set will still contain 'v'. + -- However, this method isn't perfect, because it is possible for the solver + -- to add new unnecessary variables to the conflict set on any run. This + -- function prevents the conflict set from growing by checking that the new + -- conflict set is a subset of the old one and falling back to using the old + -- conflict set when that check fails. + tryToRemoveOneVar :: Var QPN + -> ConflictSet + -> ConflictMap + -> RetryLog String SolverFailure a + tryToRemoveOneVar v smallestKnownCS smallestKnownCM = + continueWith ("Trying to remove variable " ++ varStr ++ " from the " + ++ "conflict set.") $ + retry (runSolver sc') $ \case + err@(ExhaustiveSearch cs' _) + | CS.toSet cs' `isSubsetOf` CS.toSet smallestKnownCS -> + let msg = if not $ CS.member v cs' + then "Successfully removed " ++ varStr ++ " from " + ++ "the conflict set." + else "Failed to remove " ++ varStr ++ " from the " + ++ "conflict set." + in failWith (msg ++ " Continuing with " ++ showCS cs' ++ ".") err + | otherwise -> + failWith ("Failed to find a smaller conflict set. The new " + ++ "conflict set is not a subset of the previous " + ++ "conflict set: " ++ showCS cs') $ + ExhaustiveSearch smallestKnownCS smallestKnownCM + BackjumpLimitReached -> + failWith ("Reached backjump limit while minimizing conflict set.") + BackjumpLimitReached + where + varStr = "\"" ++ showVar v ++ "\"" + showCS cs' = "{" ++ showConflictSet cs' ++ "}" + + sc' = sc { goalOrder = Just goalOrder' } + + goalOrder' = + preferGoalsFromConflictSet (v `CS.delete` smallestKnownCS) + <> preferGoal v + <> fromMaybe mempty (goalOrder sc) + + -- Like 'retry', except that it only applies the input function when the + -- backjump limit has not been reached. + retryNoSolution :: RetryLog step SolverFailure done + -> (ConflictSet -> ConflictMap -> RetryLog step SolverFailure done) + -> RetryLog step SolverFailure done + retryNoSolution lg f = retry lg $ \case + ExhaustiveSearch cs' cm' -> f cs' cm' + BackjumpLimitReached -> fromProgress (Fail BackjumpLimitReached) + -- | Goal ordering that chooses goals contained in the conflict set before -- other goals. preferGoalsFromConflictSet :: ConflictSet -> Variable QPN -> Variable QPN -> Ordering -preferGoalsFromConflictSet cs = - comparing $ \v -> not $ CS.member (toVar v) cs - where - toVar :: Variable QPN -> Var QPN - toVar (PackageVar qpn) = P qpn - toVar (FlagVar qpn fn) = F (FN qpn fn) - toVar (StanzaVar qpn sn) = S (SN qpn sn) +preferGoalsFromConflictSet cs = comparing $ \v -> not $ CS.member (toVar v) cs + +-- | Goal ordering that chooses the given goal first. +preferGoal :: Var QPN -> Variable QPN -> Variable QPN -> Ordering +preferGoal preferred = comparing $ \v -> toVar v /= preferred + +toVar :: Variable QPN -> Var QPN +toVar (PackageVar qpn) = P qpn +toVar (FlagVar qpn fn) = F (FN qpn fn) +toVar (StanzaVar qpn sn) = S (SN qpn sn) finalErrorMsg :: Verbosity -> Maybe Int -> SolverFailure -> String finalErrorMsg verbosity mbj failure = diff --git a/cabal-install/Distribution/Solver/Modular/ConflictSet.hs b/cabal-install/Distribution/Solver/Modular/ConflictSet.hs index 46e25f76c4d..6c8b47aa96a 100644 --- a/cabal-install/Distribution/Solver/Modular/ConflictSet.hs +++ b/cabal-install/Distribution/Solver/Modular/ConflictSet.hs @@ -18,10 +18,12 @@ module Distribution.Solver.Modular.ConflictSet ( , showCSSortedByFrequency , showCSWithFrequency -- Set-like operations + , toSet , toList , union , unions , insert + , delete , empty , singleton , member @@ -98,6 +100,9 @@ showCS showCount cm = Set-like operations -------------------------------------------------------------------------------} +toSet :: ConflictSet -> Set (Var QPN) +toSet = conflictSetToSet + toList :: ConflictSet -> [Var QPN] toList = S.toList . conflictSetToSet @@ -137,6 +142,11 @@ insert var cs = CS { #endif } +delete :: Var QPN -> ConflictSet -> ConflictSet +delete var cs = CS { + conflictSetToSet = S.delete var (conflictSetToSet cs) + } + empty :: #ifdef DEBUG_CONFLICT_SETS (?loc :: CallStack) => From f62fcf7e214db652f3f5506d2e2f92b39ce07e0a Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sun, 11 Nov 2018 20:43:03 -0800 Subject: [PATCH 04/10] Solver: Add a flag to control minimizing the conflict set. Minimizing the conflict set requires rerunning the solver multiple times, which can be time consuming. This commit adds a flag, --minimize-conflict-set, that defaults to false. We should probably add a way to limit the total run time before turning the feature on by default, such as applying the backjump limit to the combined solver runs. --- cabal-install/Distribution/Client/Config.hs | 1 + .../Distribution/Client/Dependency.hs | 21 +++++-- cabal-install/Distribution/Client/Fetch.hs | 3 + cabal-install/Distribution/Client/Freeze.hs | 3 + cabal-install/Distribution/Client/Install.hs | 3 + .../Distribution/Client/ProjectConfig.hs | 2 + .../Client/ProjectConfig/Legacy.hs | 5 +- .../Client/ProjectConfig/Types.hs | 2 + .../Distribution/Client/ProjectPlanning.hs | 2 + cabal-install/Distribution/Client/Setup.hs | 22 ++++++- cabal-install/Distribution/Solver/Modular.hs | 60 ++++++++++--------- .../Distribution/Solver/Modular/Solver.hs | 1 + .../Distribution/Solver/Types/Settings.hs | 5 ++ .../Distribution/Client/ProjectConfig.hs | 47 ++++++++------- 14 files changed, 121 insertions(+), 56 deletions(-) diff --git a/cabal-install/Distribution/Client/Config.hs b/cabal-install/Distribution/Client/Config.hs index 5e53e0151fc..197555f01d7 100644 --- a/cabal-install/Distribution/Client/Config.hs +++ b/cabal-install/Distribution/Client/Config.hs @@ -254,6 +254,7 @@ instance Semigroup SavedConfig where installMaxBackjumps = combine installMaxBackjumps, installReorderGoals = combine installReorderGoals, installCountConflicts = combine installCountConflicts, + installMinimizeConflictSet = combine installMinimizeConflictSet, installIndependentGoals = combine installIndependentGoals, installShadowPkgs = combine installShadowPkgs, installStrongFlags = combine installStrongFlags, diff --git a/cabal-install/Distribution/Client/Dependency.hs b/cabal-install/Distribution/Client/Dependency.hs index d58eb848c43..c6e4c16824f 100644 --- a/cabal-install/Distribution/Client/Dependency.hs +++ b/cabal-install/Distribution/Client/Dependency.hs @@ -47,6 +47,7 @@ module Distribution.Client.Dependency ( setPreferenceDefault, setReorderGoals, setCountConflicts, + setMinimizeConflictSet, setIndependentGoals, setAvoidReinstalls, setShadowPkgs, @@ -158,6 +159,7 @@ data DepResolverParams = DepResolverParams { depResolverSourcePkgIndex :: PackageIndex.PackageIndex UnresolvedSourcePackage, depResolverReorderGoals :: ReorderGoals, depResolverCountConflicts :: CountConflicts, + depResolverMinimizeConflictSet :: MinimizeConflictSet, depResolverIndependentGoals :: IndependentGoals, depResolverAvoidReinstalls :: AvoidReinstalls, depResolverShadowPkgs :: ShadowPkgs, @@ -195,6 +197,7 @@ showDepResolverParams p = ++ "\nstrategy: " ++ show (depResolverPreferenceDefault p) ++ "\nreorder goals: " ++ show (asBool (depResolverReorderGoals p)) ++ "\ncount conflicts: " ++ show (asBool (depResolverCountConflicts p)) + ++ "\nminimize conflict set: " ++ show (asBool (depResolverMinimizeConflictSet p)) ++ "\nindependent goals: " ++ show (asBool (depResolverIndependentGoals p)) ++ "\navoid reinstalls: " ++ show (asBool (depResolverAvoidReinstalls p)) ++ "\nshadow packages: " ++ show (asBool (depResolverShadowPkgs p)) @@ -251,6 +254,7 @@ basicDepResolverParams installedPkgIndex sourcePkgIndex = depResolverSourcePkgIndex = sourcePkgIndex, depResolverReorderGoals = ReorderGoals False, depResolverCountConflicts = CountConflicts True, + depResolverMinimizeConflictSet = MinimizeConflictSet False, depResolverIndependentGoals = IndependentGoals False, depResolverAvoidReinstalls = AvoidReinstalls False, depResolverShadowPkgs = ShadowPkgs False, @@ -306,6 +310,12 @@ setCountConflicts count params = depResolverCountConflicts = count } +setMinimizeConflictSet :: MinimizeConflictSet -> DepResolverParams -> DepResolverParams +setMinimizeConflictSet minimize params = + params { + depResolverMinimizeConflictSet = minimize + } + setIndependentGoals :: IndependentGoals -> DepResolverParams -> DepResolverParams setIndependentGoals indep params = params { @@ -745,8 +755,7 @@ resolveDependencies platform comp pkgConfigDB solver params = Step (showDepResolverParams finalparams) $ fmap (validateSolverResult platform comp indGoals) - $ runSolver solver (SolverConfig reordGoals cntConflicts - indGoals noReinstalls + $ runSolver solver (SolverConfig reordGoals cntConflicts minimize indGoals noReinstalls shadowing strFlags allowBootLibs onlyConstrained_ maxBkjumps enableBj solveExes order verbosity (PruneAfterFirstSuccess False)) platform comp installedPkgIndex sourcePkgIndex @@ -760,6 +769,7 @@ resolveDependencies platform comp pkgConfigDB solver params = sourcePkgIndex reordGoals cntConflicts + minimize indGoals noReinstalls shadowing @@ -1005,9 +1015,10 @@ resolveWithoutDependencies :: DepResolverParams -> Either [ResolveNoDepsError] [UnresolvedSourcePackage] resolveWithoutDependencies (DepResolverParams targets constraints prefs defpref installedPkgIndex sourcePkgIndex - _reorderGoals _countConflicts _indGoals _avoidReinstalls - _shadowing _strFlags _maxBjumps _enableBj - _solveExes _allowBootLibInstalls _onlyConstrained _order _verbosity) = + _reorderGoals _countConflicts _minimizeConflictSet + _indGoals _avoidReinstalls _shadowing _strFlags + _maxBjumps _enableBj _solveExes + _allowBootLibInstalls _onlyConstrained _order _verbosity) = collectEithers $ map selectPackage (Set.toList targets) where selectPackage :: PackageName -> Either ResolveNoDepsError UnresolvedSourcePackage diff --git a/cabal-install/Distribution/Client/Fetch.hs b/cabal-install/Distribution/Client/Fetch.hs index 31d13275e6d..ac0a74a907d 100644 --- a/cabal-install/Distribution/Client/Fetch.hs +++ b/cabal-install/Distribution/Client/Fetch.hs @@ -162,6 +162,8 @@ planPackages verbosity comp platform fetchFlags . setCountConflicts countConflicts + . setMinimizeConflictSet minimizeConflictSet + . setShadowPkgs shadowPkgs . setStrongFlags strongFlags @@ -197,6 +199,7 @@ planPackages verbosity comp platform fetchFlags reorderGoals = fromFlag (fetchReorderGoals fetchFlags) countConflicts = fromFlag (fetchCountConflicts fetchFlags) + minimizeConflictSet = fromFlag (fetchMinimizeConflictSet fetchFlags) independentGoals = fromFlag (fetchIndependentGoals fetchFlags) shadowPkgs = fromFlag (fetchShadowPkgs fetchFlags) strongFlags = fromFlag (fetchStrongFlags fetchFlags) diff --git a/cabal-install/Distribution/Client/Freeze.hs b/cabal-install/Distribution/Client/Freeze.hs index 461568cbee5..3b8b2c193ab 100644 --- a/cabal-install/Distribution/Client/Freeze.hs +++ b/cabal-install/Distribution/Client/Freeze.hs @@ -175,6 +175,8 @@ planPackages verbosity comp platform mSandboxPkgInfo freezeFlags . setCountConflicts countConflicts + . setMinimizeConflictSet minimizeConflictSet + . setShadowPkgs shadowPkgs . setStrongFlags strongFlags @@ -205,6 +207,7 @@ planPackages verbosity comp platform mSandboxPkgInfo freezeFlags reorderGoals = fromFlag (freezeReorderGoals freezeFlags) countConflicts = fromFlag (freezeCountConflicts freezeFlags) + minimizeConflictSet = fromFlag (freezeMinimizeConflictSet freezeFlags) independentGoals = fromFlag (freezeIndependentGoals freezeFlags) shadowPkgs = fromFlag (freezeShadowPkgs freezeFlags) strongFlags = fromFlag (freezeStrongFlags freezeFlags) diff --git a/cabal-install/Distribution/Client/Install.hs b/cabal-install/Distribution/Client/Install.hs index 0d3391a2377..df2c7bb4b7e 100644 --- a/cabal-install/Distribution/Client/Install.hs +++ b/cabal-install/Distribution/Client/Install.hs @@ -389,6 +389,8 @@ planPackages verbosity comp platform mSandboxPkgInfo solver . setCountConflicts countConflicts + . setMinimizeConflictSet minimizeConflictSet + . setAvoidReinstalls avoidReinstalls . setShadowPkgs shadowPkgs @@ -455,6 +457,7 @@ planPackages verbosity comp platform mSandboxPkgInfo solver fromFlag (installReinstall installFlags) reorderGoals = fromFlag (installReorderGoals installFlags) countConflicts = fromFlag (installCountConflicts installFlags) + minimizeConflictSet = fromFlag (installMinimizeConflictSet installFlags) independentGoals = fromFlag (installIndependentGoals installFlags) avoidReinstalls = fromFlag (installAvoidReinstalls installFlags) shadowPkgs = fromFlag (installShadowPkgs installFlags) diff --git a/cabal-install/Distribution/Client/ProjectConfig.hs b/cabal-install/Distribution/Client/ProjectConfig.hs index 8e05e02f067..9417aeb7d05 100644 --- a/cabal-install/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/Distribution/Client/ProjectConfig.hs @@ -238,6 +238,7 @@ resolveSolverSettings ProjectConfig{ | otherwise -> Just n solverSettingReorderGoals = fromFlag projectConfigReorderGoals solverSettingCountConflicts = fromFlag projectConfigCountConflicts + solverSettingMinimizeConflictSet = fromFlag projectConfigMinimizeConflictSet solverSettingStrongFlags = fromFlag projectConfigStrongFlags solverSettingAllowBootLibInstalls = fromFlag projectConfigAllowBootLibInstalls solverSettingOnlyConstrained = fromFlag projectConfigOnlyConstrained @@ -258,6 +259,7 @@ resolveSolverSettings ProjectConfig{ projectConfigMaxBackjumps = Flag defaultMaxBackjumps, projectConfigReorderGoals = Flag (ReorderGoals False), projectConfigCountConflicts = Flag (CountConflicts True), + projectConfigMinimizeConflictSet = Flag (MinimizeConflictSet False), projectConfigStrongFlags = Flag (StrongFlags False), projectConfigAllowBootLibInstalls = Flag (AllowBootLibInstalls False), projectConfigOnlyConstrained = Flag OnlyConstrainedNone, diff --git a/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs index 1061868ca7b..660775effbd 100644 --- a/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs @@ -352,6 +352,7 @@ convertLegacyAllPackageFlags globalFlags configFlags --installUpgradeDeps = projectConfigUpgradeDeps, installReorderGoals = projectConfigReorderGoals, installCountConflicts = projectConfigCountConflicts, + installMinimizeConflictSet = projectConfigMinimizeConflictSet, installPerComponent = projectConfigPerComponent, installIndependentGoals = projectConfigIndependentGoals, --installShadowPkgs = projectConfigShadowPkgs, @@ -576,6 +577,7 @@ convertToLegacySharedConfig installUpgradeDeps = mempty, --projectConfigUpgradeDeps, installReorderGoals = projectConfigReorderGoals, installCountConflicts = projectConfigCountConflicts, + installMinimizeConflictSet = projectConfigMinimizeConflictSet, installIndependentGoals = projectConfigIndependentGoals, installShadowPkgs = mempty, --projectConfigShadowPkgs, installStrongFlags = projectConfigStrongFlags, @@ -952,7 +954,8 @@ legacySharedConfigFieldDescrs = , "remote-build-reporting", "report-planning-failure" , "one-shot", "jobs", "keep-going", "offline", "per-component" -- solver flags: - , "max-backjumps", "reorder-goals", "count-conflicts", "independent-goals" + , "max-backjumps", "reorder-goals", "count-conflicts" + , "minimize-conflict-set", "independent-goals" , "strong-flags" , "allow-boot-library-installs", "reject-unconstrained-dependencies", "index-state" ] . commandOptionsToFields diff --git a/cabal-install/Distribution/Client/ProjectConfig/Types.hs b/cabal-install/Distribution/Client/ProjectConfig/Types.hs index f32b5abf963..7d4698eb91c 100644 --- a/cabal-install/Distribution/Client/ProjectConfig/Types.hs +++ b/cabal-install/Distribution/Client/ProjectConfig/Types.hs @@ -193,6 +193,7 @@ data ProjectConfigShared projectConfigMaxBackjumps :: Flag Int, projectConfigReorderGoals :: Flag ReorderGoals, projectConfigCountConflicts :: Flag CountConflicts, + projectConfigMinimizeConflictSet :: Flag MinimizeConflictSet, projectConfigStrongFlags :: Flag StrongFlags, projectConfigAllowBootLibInstalls :: Flag AllowBootLibInstalls, projectConfigOnlyConstrained :: Flag OnlyConstrained, @@ -382,6 +383,7 @@ data SolverSettings solverSettingMaxBackjumps :: Maybe Int, solverSettingReorderGoals :: ReorderGoals, solverSettingCountConflicts :: CountConflicts, + solverSettingMinimizeConflictSet :: MinimizeConflictSet, solverSettingStrongFlags :: StrongFlags, solverSettingAllowBootLibInstalls :: AllowBootLibInstalls, solverSettingOnlyConstrained :: OnlyConstrained, diff --git a/cabal-install/Distribution/Client/ProjectPlanning.hs b/cabal-install/Distribution/Client/ProjectPlanning.hs index 3af11789ef4..efa07cfbc55 100644 --- a/cabal-install/Distribution/Client/ProjectPlanning.hs +++ b/cabal-install/Distribution/Client/ProjectPlanning.hs @@ -951,6 +951,8 @@ planPackages verbosity comp platform solver SolverSettings{..} . setCountConflicts solverSettingCountConflicts + . setMinimizeConflictSet solverSettingMinimizeConflictSet + --TODO: [required eventually] should only be configurable for --custom installs -- . setAvoidReinstalls solverSettingAvoidReinstalls diff --git a/cabal-install/Distribution/Client/Setup.hs b/cabal-install/Distribution/Client/Setup.hs index 864bcac584a..9107f166e2e 100644 --- a/cabal-install/Distribution/Client/Setup.hs +++ b/cabal-install/Distribution/Client/Setup.hs @@ -973,6 +973,7 @@ data FetchFlags = FetchFlags { fetchMaxBackjumps :: Flag Int, fetchReorderGoals :: Flag ReorderGoals, fetchCountConflicts :: Flag CountConflicts, + fetchMinimizeConflictSet :: Flag MinimizeConflictSet, fetchIndependentGoals :: Flag IndependentGoals, fetchShadowPkgs :: Flag ShadowPkgs, fetchStrongFlags :: Flag StrongFlags, @@ -992,6 +993,7 @@ defaultFetchFlags = FetchFlags { fetchMaxBackjumps = Flag defaultMaxBackjumps, fetchReorderGoals = Flag (ReorderGoals False), fetchCountConflicts = Flag (CountConflicts True), + fetchMinimizeConflictSet = Flag (MinimizeConflictSet False), fetchIndependentGoals = Flag (IndependentGoals False), fetchShadowPkgs = Flag (ShadowPkgs False), fetchStrongFlags = Flag (StrongFlags False), @@ -1053,6 +1055,7 @@ fetchCommand = CommandUI { fetchMaxBackjumps (\v flags -> flags { fetchMaxBackjumps = v }) fetchReorderGoals (\v flags -> flags { fetchReorderGoals = v }) fetchCountConflicts (\v flags -> flags { fetchCountConflicts = v }) + fetchMinimizeConflictSet (\v flags -> flags { fetchMinimizeConflictSet = v }) fetchIndependentGoals (\v flags -> flags { fetchIndependentGoals = v }) fetchShadowPkgs (\v flags -> flags { fetchShadowPkgs = v }) fetchStrongFlags (\v flags -> flags { fetchStrongFlags = v }) @@ -1073,6 +1076,7 @@ data FreezeFlags = FreezeFlags { freezeMaxBackjumps :: Flag Int, freezeReorderGoals :: Flag ReorderGoals, freezeCountConflicts :: Flag CountConflicts, + freezeMinimizeConflictSet :: Flag MinimizeConflictSet, freezeIndependentGoals :: Flag IndependentGoals, freezeShadowPkgs :: Flag ShadowPkgs, freezeStrongFlags :: Flag StrongFlags, @@ -1090,6 +1094,7 @@ defaultFreezeFlags = FreezeFlags { freezeMaxBackjumps = Flag defaultMaxBackjumps, freezeReorderGoals = Flag (ReorderGoals False), freezeCountConflicts = Flag (CountConflicts True), + freezeMinimizeConflictSet = Flag (MinimizeConflictSet False), freezeIndependentGoals = Flag (IndependentGoals False), freezeShadowPkgs = Flag (ShadowPkgs False), freezeStrongFlags = Flag (StrongFlags False), @@ -1142,6 +1147,7 @@ freezeCommand = CommandUI { freezeMaxBackjumps (\v flags -> flags { freezeMaxBackjumps = v }) freezeReorderGoals (\v flags -> flags { freezeReorderGoals = v }) freezeCountConflicts (\v flags -> flags { freezeCountConflicts = v }) + freezeMinimizeConflictSet (\v flags -> flags { freezeMinimizeConflictSet = v }) freezeIndependentGoals (\v flags -> flags { freezeIndependentGoals = v }) freezeShadowPkgs (\v flags -> flags { freezeShadowPkgs = v }) freezeStrongFlags (\v flags -> flags { freezeStrongFlags = v }) @@ -1711,6 +1717,7 @@ data InstallFlags = InstallFlags { installMaxBackjumps :: Flag Int, installReorderGoals :: Flag ReorderGoals, installCountConflicts :: Flag CountConflicts, + installMinimizeConflictSet :: Flag MinimizeConflictSet, installIndependentGoals :: Flag IndependentGoals, installShadowPkgs :: Flag ShadowPkgs, installStrongFlags :: Flag StrongFlags, @@ -1757,6 +1764,7 @@ defaultInstallFlags = InstallFlags { installMaxBackjumps = Flag defaultMaxBackjumps, installReorderGoals = Flag (ReorderGoals False), installCountConflicts = Flag (CountConflicts True), + installMinimizeConflictSet = Flag (MinimizeConflictSet False), installIndependentGoals= Flag (IndependentGoals False), installShadowPkgs = Flag (ShadowPkgs False), installStrongFlags = Flag (StrongFlags False), @@ -1968,6 +1976,7 @@ installOptions showOrParseArgs = installMaxBackjumps (\v flags -> flags { installMaxBackjumps = v }) installReorderGoals (\v flags -> flags { installReorderGoals = v }) installCountConflicts (\v flags -> flags { installCountConflicts = v }) + installMinimizeConflictSet (\v flags -> flags { installMinimizeConflictSet = v }) installIndependentGoals (\v flags -> flags { installIndependentGoals = v }) installShadowPkgs (\v flags -> flags { installShadowPkgs = v }) installStrongFlags (\v flags -> flags { installStrongFlags = v }) @@ -2778,14 +2787,16 @@ optionSolverFlags :: ShowOrParseArgs -> (flags -> Flag Int ) -> (Flag Int -> flags -> flags) -> (flags -> Flag ReorderGoals) -> (Flag ReorderGoals -> flags -> flags) -> (flags -> Flag CountConflicts) -> (Flag CountConflicts -> flags -> flags) + -> (flags -> Flag MinimizeConflictSet) -> (Flag MinimizeConflictSet -> flags -> flags) -> (flags -> Flag IndependentGoals) -> (Flag IndependentGoals -> flags -> flags) -> (flags -> Flag ShadowPkgs) -> (Flag ShadowPkgs -> flags -> flags) -> (flags -> Flag StrongFlags) -> (Flag StrongFlags -> flags -> flags) -> (flags -> Flag AllowBootLibInstalls) -> (Flag AllowBootLibInstalls -> flags -> flags) -> (flags -> Flag OnlyConstrained) -> (Flag OnlyConstrained -> flags -> flags) -> [OptionField flags] -optionSolverFlags showOrParseArgs getmbj setmbj getrg setrg getcc setcc getig setig - getsip setsip getstrfl setstrfl getib setib getoc setoc = +optionSolverFlags showOrParseArgs getmbj setmbj getrg setrg getcc setcc + getmc setmc getig setig getsip setsip getstrfl setstrfl + getib setib getoc setoc = [ option [] ["max-backjumps"] ("Maximum number of backjumps allowed while solving (default: " ++ show defaultMaxBackjumps ++ "). Use a negative number to enable unlimited backtracking. Use 0 to disable backtracking completely.") getmbj setmbj @@ -2801,6 +2812,13 @@ optionSolverFlags showOrParseArgs getmbj setmbj getrg setrg getcc setcc getig se (fmap asBool . getcc) (setcc . fmap CountConflicts) (yesNoOpt showOrParseArgs) + , option [] ["minimize-conflict-set"] + ("When there is no solution, try to improve the error message by finding " + ++ "a minimal conflict set (default: false). May increase run time " + ++ "significantly.") + (fmap asBool . getmc) + (setmc . fmap MinimizeConflictSet) + (yesNoOpt showOrParseArgs) , option [] ["independent-goals"] "Treat several goals on the command line as independent. If several goals depend on the same package, different versions can be chosen." (fmap asBool . getig) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 4afc85e474b..35d8850306e 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -50,6 +50,8 @@ import Distribution.Solver.Types.Progress import Distribution.Solver.Types.Variable import Distribution.System ( Platform(..) ) +import Distribution.Simple.Setup + ( BooleanFlag(..) ) import Distribution.Simple.Utils ( ordNubBy ) import Distribution.Verbosity @@ -84,16 +86,17 @@ modularResolver sc (Platform arch os) cinfo iidx sidx pkgConfigDB pprefs pcs pns -- -- When there is no solution, we produce the error message by rerunning the -- solver but making it prefer the goals from the final conflict set from the --- first run. We also set the backjump limit to 0, so that the log stops at the --- first backjump and is relatively short. Preferring goals from the final --- conflict set increases the probability that the log to the first backjump --- contains package, flag, and stanza choices that are relevant to the final --- failure. The solver shouldn't need to choose any packages that aren't in the --- final conflict set. (For every variable in the final conflict set, the final --- conflict set should also contain the variable that introduced that variable. --- The solver can then follow that chain of variables in reverse order from the --- user target to the conflict.) However, it is possible that the conflict set --- contains unnecessary variables. +-- first run (or a subset of the final conflict set with +-- --minimize-conflict-set). We also set the backjump limit to 0, so that the +-- log stops at the first backjump and is relatively short. Preferring goals +-- from the final conflict set increases the probability that the log to the +-- first backjump contains package, flag, and stanza choices that are relevant +-- to the final failure. The solver shouldn't need to choose any packages that +-- aren't in the final conflict set. (For every variable in the final conflict +-- set, the final conflict set should also contain the variable that introduced +-- that variable. The solver can then follow that chain of variables in reverse +-- order from the user target to the conflict.) However, it is possible that the +-- conflict set contains unnecessary variables. -- -- Producing an error message when the solver reaches the backjump limit is more -- complicated. There is no final conflict set, so we create one for the minimal @@ -134,23 +137,26 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = -> SolverFailure -> RetryLog String String (Assignment, RevDepMap) createErrorMsg verbosity mbj failure@(ExhaustiveSearch cs cm) = - continueWith ("Found no solution after exhaustively searching the " - ++ "dependency tree. Rerunning the dependency solver " - ++ "to minimize the conflict set ({" - ++ showConflictSet cs ++ "}).") $ - retry (tryToMinimizeConflictSet (runSolver printFullLog) sc cs cm) $ - \case - ExhaustiveSearch cs' cm' -> - fromProgress $ Fail $ - rerunSolverForErrorMsg cs' - ++ finalErrorMsg verbosity mbj (ExhaustiveSearch cs' cm') - BackjumpLimitReached -> - fromProgress $ Fail $ - "Reached backjump limit while trying to minimize the " - ++ "conflict set to create a better error message. " - ++ "Original error message:\n" - ++ rerunSolverForErrorMsg cs - ++ finalErrorMsg verbosity mbj failure + if asBool $ minimizeConflictSet sc + then continueWith ("Found no solution after exhaustively searching the " + ++ "dependency tree. Rerunning the dependency solver " + ++ "to minimize the conflict set ({" + ++ showConflictSet cs ++ "}).") $ + retry (tryToMinimizeConflictSet (runSolver printFullLog) sc cs cm) $ + \case + ExhaustiveSearch cs' cm' -> + fromProgress $ Fail $ + rerunSolverForErrorMsg cs' + ++ finalErrorMsg verbosity mbj (ExhaustiveSearch cs' cm') + BackjumpLimitReached -> + fromProgress $ Fail $ + "Reached backjump limit while trying to minimize the " + ++ "conflict set to create a better error message. " + ++ "Original error message:\n" + ++ rerunSolverForErrorMsg cs + ++ finalErrorMsg verbosity mbj failure + else fromProgress $ Fail $ + rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure createErrorMsg verbosity mbj failure@BackjumpLimitReached = continueWith ("Backjump limit reached. Rerunning dependency solver to generate " diff --git a/cabal-install/Distribution/Solver/Modular/Solver.hs b/cabal-install/Distribution/Solver/Modular/Solver.hs index 73ad9d70175..b0e40cb9a0a 100644 --- a/cabal-install/Distribution/Solver/Modular/Solver.hs +++ b/cabal-install/Distribution/Solver/Modular/Solver.hs @@ -57,6 +57,7 @@ import Debug.Trace.Tree.Assoc (Assoc(..)) data SolverConfig = SolverConfig { reorderGoals :: ReorderGoals, countConflicts :: CountConflicts, + minimizeConflictSet :: MinimizeConflictSet, independentGoals :: IndependentGoals, avoidReinstalls :: AvoidReinstalls, shadowPkgs :: ShadowPkgs, diff --git a/cabal-install/Distribution/Solver/Types/Settings.hs b/cabal-install/Distribution/Solver/Types/Settings.hs index 635caa7fbbc..94a99879489 100644 --- a/cabal-install/Distribution/Solver/Types/Settings.hs +++ b/cabal-install/Distribution/Solver/Types/Settings.hs @@ -3,6 +3,7 @@ module Distribution.Solver.Types.Settings ( ReorderGoals(..) , IndependentGoals(..) + , MinimizeConflictSet(..) , AvoidReinstalls(..) , ShadowPkgs(..) , StrongFlags(..) @@ -28,6 +29,9 @@ newtype ReorderGoals = ReorderGoals Bool newtype CountConflicts = CountConflicts Bool deriving (BooleanFlag, Eq, Generic, Show) +newtype MinimizeConflictSet = MinimizeConflictSet Bool + deriving (BooleanFlag, Eq, Generic, Show) + newtype IndependentGoals = IndependentGoals Bool deriving (BooleanFlag, Eq, Generic, Show) @@ -59,6 +63,7 @@ newtype SolveExecutables = SolveExecutables Bool instance Binary ReorderGoals instance Binary CountConflicts instance Binary IndependentGoals +instance Binary MinimizeConflictSet instance Binary AvoidReinstalls instance Binary ShadowPkgs instance Binary StrongFlags diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index fe43a236670..c675b8ff4c6 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -438,7 +438,7 @@ instance Arbitrary ProjectConfigShared where <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary - <*> arbitrary + <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary @@ -468,14 +468,15 @@ instance Arbitrary ProjectConfigShared where , projectConfigMaxBackjumps = x16 , projectConfigReorderGoals = x17 , projectConfigCountConflicts = x18 - , projectConfigStrongFlags = x19 - , projectConfigAllowBootLibInstalls = x20 - , projectConfigOnlyConstrained = x21 - , projectConfigPerComponent = x22 - , projectConfigIndependentGoals = x23 - , projectConfigConfigFile = x24 - , projectConfigProgPathExtra = x25 - , projectConfigStoreDir = x26 } = + , projectConfigMinimizeConflictSet = x19 + , projectConfigStrongFlags = x20 + , projectConfigAllowBootLibInstalls = x21 + , projectConfigOnlyConstrained = x22 + , projectConfigPerComponent = x23 + , projectConfigIndependentGoals = x24 + , projectConfigConfigFile = x25 + , projectConfigProgPathExtra = x26 + , projectConfigStoreDir = x27 } = [ ProjectConfigShared { projectConfigDistDir = x00' , projectConfigProjectFile = x01' , projectConfigHcFlavor = x02' @@ -495,25 +496,26 @@ instance Arbitrary ProjectConfigShared where , projectConfigMaxBackjumps = x16' , projectConfigReorderGoals = x17' , projectConfigCountConflicts = x18' - , projectConfigStrongFlags = x19' - , projectConfigAllowBootLibInstalls = x20' - , projectConfigOnlyConstrained = x21' - , projectConfigPerComponent = x22' - , projectConfigIndependentGoals = x23' - , projectConfigConfigFile = x24' - , projectConfigProgPathExtra = x25' - , projectConfigStoreDir = x26' } + , projectConfigMinimizeConflictSet = x19' + , projectConfigStrongFlags = x20' + , projectConfigAllowBootLibInstalls = x21' + , projectConfigOnlyConstrained = x22' + , projectConfigPerComponent = x23' + , projectConfigIndependentGoals = x24' + , projectConfigConfigFile = x25' + , projectConfigProgPathExtra = x26' + , projectConfigStoreDir = x27' } | ((x00', x01', x02', x03', x04'), (x05', x06', x07', x08', x09'), (x10', x11', x12', x13', x14', x15'), - (x16', x17', x18', x19', x20'), - x21', x22', x23', x24', x25', x26') + (x16', x17', x18', x19', x20', x21'), + x22', x23', x24', x25', x26', x27') <- shrink ((x00, x01, x02, fmap NonEmpty x03, fmap NonEmpty x04), (x05, x06, x07, x08, preShrink_Constraints x09), (x10, x11, x12, x13, x14, x15), - (x16, x17, x18, x19, x20), - x21, x22, x23, x24, x25, x26) + (x16, x17, x18, x19, x20, x21), + x22, x23, x24, x25, x26, x27) ] where preShrink_Constraints = map fst @@ -835,6 +837,9 @@ instance Arbitrary ReorderGoals where instance Arbitrary CountConflicts where arbitrary = CountConflicts <$> arbitrary +instance Arbitrary MinimizeConflictSet where + arbitrary = MinimizeConflictSet <$> arbitrary + instance Arbitrary IndependentGoals where arbitrary = IndependentGoals <$> arbitrary From c31f7a41134343ac125aa73196a7c86105c21c20 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sun, 11 Nov 2018 20:43:03 -0800 Subject: [PATCH 05/10] Add --minimize-conflict-set to solver DSL. --- .../UnitTests/Distribution/Solver/Modular/DSL.hs | 7 +++++-- .../Distribution/Solver/Modular/DSL/TestCaseUtils.hs | 10 +++++++++- .../Distribution/Solver/Modular/QuickCheck.hs | 5 +++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs b/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs index 3396a540cb4..75735c08064 100644 --- a/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs +++ b/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs @@ -646,6 +646,7 @@ exResolve :: ExampleDb -> [ExamplePkgName] -> Maybe Int -> CountConflicts + -> MinimizeConflictSet -> IndependentGoals -> ReorderGoals -> AllowBootLibInstalls @@ -658,8 +659,9 @@ exResolve :: ExampleDb -> C.Verbosity -> EnableAllTests -> Progress String String CI.SolverInstallPlan.SolverInstallPlan -exResolve db exts langs pkgConfigDb targets mbj countConflicts indepGoals - reorder allowBootLibInstalls onlyConstrained enableBj solveExes goalOrder constraints +exResolve db exts langs pkgConfigDb targets mbj countConflicts + minimizeConflictSet indepGoals reorder allowBootLibInstalls + onlyConstrained enableBj solveExes goalOrder constraints prefs verbosity enableAllTests = resolveDependencies C.buildPlatform compiler pkgConfigDb Modular params where @@ -684,6 +686,7 @@ exResolve db exts langs pkgConfigDb targets mbj countConflicts indepGoals $ addConstraints (fmap toLpc enableTests) $ addPreferences (fmap toPref prefs) $ setCountConflicts countConflicts + $ setMinimizeConflictSet minimizeConflictSet $ setIndependentGoals indepGoals $ setReorderGoals reorder $ setMaxBackjumps mbj diff --git a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs index ef8f9cb6657..2f8c7a69f59 100644 --- a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs +++ b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs @@ -4,6 +4,7 @@ module UnitTests.Distribution.Solver.Modular.DSL.TestCaseUtils ( SolverTest , SolverResult(..) , maxBackjumps + , minimizeConflictSet , independentGoals , allowBootLibInstalls , onlyConstrained @@ -53,6 +54,10 @@ import UnitTests.Options maxBackjumps :: Maybe Int -> SolverTest -> SolverTest maxBackjumps mbj test = test { testMaxBackjumps = mbj } +minimizeConflictSet :: SolverTest -> SolverTest +minimizeConflictSet test = + test { testMinimizeConflictSet = MinimizeConflictSet True } + -- | Combinator to turn on --independent-goals behavior, i.e. solve -- for the goals as if we were solving for each goal independently. independentGoals :: SolverTest -> SolverTest @@ -100,6 +105,7 @@ data SolverTest = SolverTest { , testTargets :: [String] , testResult :: SolverResult , testMaxBackjumps :: Maybe Int + , testMinimizeConflictSet :: MinimizeConflictSet , testIndepGoals :: IndependentGoals , testAllowBootLibInstalls :: AllowBootLibInstalls , testOnlyConstrained :: OnlyConstrained @@ -195,6 +201,7 @@ mkTestExtLangPC exts langs pkgConfigDb db label targets result = SolverTest { , testTargets = targets , testResult = result , testMaxBackjumps = Nothing + , testMinimizeConflictSet = MinimizeConflictSet False , testIndepGoals = IndependentGoals False , testAllowBootLibInstalls = AllowBootLibInstalls False , testOnlyConstrained = OnlyConstrainedNone @@ -216,7 +223,8 @@ runTest SolverTest{..} = askOption $ \(OptionShowSolverLog showSolverLog) -> testCase testLabel $ do let progress = exResolve testDb testSupportedExts testSupportedLangs testPkgConfigDb testTargets - testMaxBackjumps (CountConflicts True) testIndepGoals + testMaxBackjumps (CountConflicts True) + testMinimizeConflictSet testIndepGoals (ReorderGoals False) testAllowBootLibInstalls testOnlyConstrained testEnableBackjumping testSolveExecutables (sortGoals <$> testGoalOrder) testConstraints diff --git a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs index ae4589a72ed..c6cf5d44262 100644 --- a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs +++ b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs @@ -143,8 +143,9 @@ solve enableBj reorder countConflicts indep goalOrder test = -- The backjump limit prevents individual tests from using -- too much time and memory. (Just defaultMaxBackjumps) - countConflicts indep reorder (AllowBootLibInstalls False) - OnlyConstrainedNone enableBj (SolveExecutables True) (unVarOrdering <$> goalOrder) + countConflicts (MinimizeConflictSet False) indep reorder + (AllowBootLibInstalls False) OnlyConstrainedNone enableBj + (SolveExecutables True) (unVarOrdering <$> goalOrder) (testConstraints test) (testPreferences test) normal (EnableAllTests False) From dc55e7b531a616eee93d11a369424e225eb42496 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sun, 11 Nov 2018 20:43:03 -0800 Subject: [PATCH 06/10] Add unit tests for --minimize-conflict-set. --- .../Distribution/Solver/Modular/Solver.hs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs index e4be7ebb97f..d477c37e98b 100644 --- a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs +++ b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs @@ -445,6 +445,10 @@ tests = [ "Backjump limit reached (currently 1, change with --max-backjumps " ++ "or try to run with --reorder-goals).\n" ++ "Failed to generate a summarized dependency solver log due to low backjump limit." + , testMinimizeConflictSet + "minimize conflict set with --minimize-conflict-set" + , testNoMinimizeConflictSet + "show original conflict set with --no-minimize-conflict-set" ] ] where @@ -1418,6 +1422,80 @@ testSummarizedLog testName mbj expectedMsg = goals :: [ExampleVar] goals = [P QualNone pkg | pkg <- ["A", "B", "C", "D"]] +dbMinimizeConflictSet :: ExampleDb +dbMinimizeConflictSet = [ + Right $ exAv "A" 3 [ExFix "B" 2, ExFix "C" 1, ExFix "D" 2] + , Right $ exAv "A" 2 [ExFix "B" 1, ExFix "C" 2, ExFix "D" 2] + , Right $ exAv "A" 1 [ExFix "B" 1, ExFix "C" 1, ExFix "D" 2] + , Right $ exAv "B" 1 [] + , Right $ exAv "C" 1 [] + , Right $ exAv "D" 1 [] + ] + +-- | Test that the solver can find a minimal conflict set with +-- --minimize-conflict-set. In the first run, the goal order causes the solver +-- to find that A-3 conflicts with B, A-2 conflicts with C, and A-1 conflicts +-- with D. The full log should show that the original final conflict set is +-- {A, B, C, D}. Then the solver should be able to reduce the conflict set to +-- {A, D}, since all versions of A conflict with D. The summarized log should +-- only mention A and D. +testMinimizeConflictSet :: String -> TestTree +testMinimizeConflictSet testName = + runTest $ minimizeConflictSet $ goalOrder goals $ setVerbose $ + mkTest dbMinimizeConflictSet testName ["A"] $ + SolverResult checkFullLog (Left (== expectedMsg)) + where + checkFullLog :: [String] -> Bool + checkFullLog = containsInOrder [ + "[__0] fail (backjumping, conflict set: A, B, C, D)" + , "Found no solution after exhaustively searching the dependency tree. " + ++ "Rerunning the dependency solver to minimize the conflict set ({A, B, C, D})." + , "Trying to remove variable \"A\" from the conflict set." + , "Failed to remove \"A\" from the conflict set. Continuing with {A, B, C, D}." + , "Trying to remove variable \"B\" from the conflict set." + , "Successfully removed \"B\" from the conflict set. Continuing with {A, C, D}." + , "Trying to remove variable \"C\" from the conflict set." + , "Successfully removed \"C\" from the conflict set. Continuing with {A, D}." + , "Trying to remove variable \"D\" from the conflict set." + , "Failed to remove \"D\" from the conflict set. Continuing with {A, D}." + ] + + expectedMsg = + "Could not resolve dependencies:\n" + ++ "[__0] trying: A-3.0.0 (user goal)\n" + ++ "[__1] next goal: D (dependency of A)\n" + ++ "[__1] rejecting: D-1.0.0 (conflict: A => D==2.0.0)\n" + ++ "[__1] fail (backjumping, conflict set: A, D)\n" + ++ "After searching the rest of the dependency tree exhaustively, these " + ++ "were the goals I've had most trouble fulfilling: A (7), D (6)" + + goals :: [ExampleVar] + goals = [P QualNone pkg | pkg <- ["A", "B", "C", "D"]] + +-- | This test uses the same packages and goal order as testMinimizeConflictSet, +-- but it doesn't set --minimize-conflict-set. The solver should print the +-- original final conflict set and the conflict between A and B. It should also +-- suggest rerunning with --minimize-conflict-set. +testNoMinimizeConflictSet :: String -> TestTree +testNoMinimizeConflictSet testName = + runTest $ goalOrder goals $ setVerbose $ + mkTest dbMinimizeConflictSet testName ["A"] $ + solverFailure (== expectedMsg) + where + expectedMsg = + "Could not resolve dependencies:\n" + ++ "[__0] trying: A-3.0.0 (user goal)\n" + ++ "[__1] next goal: B (dependency of A)\n" + ++ "[__1] rejecting: B-1.0.0 (conflict: A => B==2.0.0)\n" + ++ "[__1] fail (backjumping, conflict set: A, B)\n" + ++ "After searching the rest of the dependency tree exhaustively, " + ++ "these were the goals I've had most trouble fulfilling: " + ++ "A (7), B (2), C (2), D (2)\n" + ++ "Try running with --minimize-conflict-set to improve the error message." + + goals :: [ExampleVar] + goals = [P QualNone pkg | pkg <- ["A", "B", "C", "D"]] + {------------------------------------------------------------------------------- Simple databases for the illustrations for the backjumping blog post -------------------------------------------------------------------------------} @@ -1700,3 +1778,12 @@ dbIssue3775 = [ Right $ exAv "A" 2 [ExFix "warp" 1] `withExe` ExExe "warp" [ExAny "A"], Right $ exAv "B" 2 [ExAny "A", ExAny "warp"] ] + +-- | Returns true if the second list contains all elements of the first list, in +-- order. +containsInOrder :: Eq a => [a] -> [a] -> Bool +containsInOrder [] _ = True +containsInOrder _ [] = False +containsInOrder (x:xs) (y:ys) + | x == y = containsInOrder xs ys + | otherwise = containsInOrder (x:xs) ys From 5ca836eb20c9866a94e9e85247e0562bb87ff28b Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sat, 17 Nov 2018 13:58:23 -0800 Subject: [PATCH 07/10] Check for presence of variable before removing it with --minimize-conflict-set. Sometimes, rerunning the solver to remove one variable from the conflict set also removes other variables. This commit checks whether each variable is still present in the conflict set before trying to remove it, which can save expensive solver reruns. --- cabal-install/Distribution/Solver/Modular.hs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 35d8850306e..789b268ad54 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -254,7 +254,12 @@ tryToMinimizeConflictSet runSolver sc cs cm = -> ConflictSet -> ConflictMap -> RetryLog String SolverFailure a - tryToRemoveOneVar v smallestKnownCS smallestKnownCM = + tryToRemoveOneVar v smallestKnownCS smallestKnownCM + -- Check whether v is still present, because it may have already been + -- removed in a previous solver rerun. + | not (v `CS.member` smallestKnownCS) = + fromProgress $ Fail $ ExhaustiveSearch smallestKnownCS smallestKnownCM + | otherwise = continueWith ("Trying to remove variable " ++ varStr ++ " from the " ++ "conflict set.") $ retry (runSolver sc') $ \case @@ -265,7 +270,9 @@ tryToMinimizeConflictSet runSolver sc cs cm = ++ "the conflict set." else "Failed to remove " ++ varStr ++ " from the " ++ "conflict set." - in failWith (msg ++ " Continuing with " ++ showCS cs' ++ ".") err + in -- Use the new conflict set, even if v wasn't removed, + -- because other variables may have been removed. + failWith (msg ++ " Continuing with " ++ showCS cs' ++ ".") err | otherwise -> failWith ("Failed to find a smaller conflict set. The new " ++ "conflict set is not a subset of the previous " From 428c0972ecc76807310f6d30cbd203d46e1f7fc8 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sat, 17 Nov 2018 13:58:23 -0800 Subject: [PATCH 08/10] Suggest running with --minimize-conflict-set in solver error message. --- cabal-install/Distribution/Solver/Modular.hs | 37 +++++++++++-------- .../Solver/Modular/ConflictSet.hs | 4 ++ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 789b268ad54..4648ab8a130 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -123,8 +123,7 @@ solve' :: SolverConfig -> Set PN -> Progress String String (Assignment, RevDepMap) solve' sc cinfo idx pkgConfigDB pprefs gcs pns = - toProgress $ retry (runSolver printFullLog sc) - (createErrorMsg (solverVerbosity sc) (maxBackjumps sc)) + toProgress $ retry (runSolver printFullLog sc) createErrorMsg where runSolver :: Bool -> SolverConfig -> RetryLog String SolverFailure (Assignment, RevDepMap) @@ -132,11 +131,9 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = displayLogMessages keepLog $ solve sc' cinfo idx pkgConfigDB pprefs gcs pns - createErrorMsg :: Verbosity - -> Maybe Int - -> SolverFailure + createErrorMsg :: SolverFailure -> RetryLog String String (Assignment, RevDepMap) - createErrorMsg verbosity mbj failure@(ExhaustiveSearch cs cm) = + createErrorMsg failure@(ExhaustiveSearch cs cm) = if asBool $ minimizeConflictSet sc then continueWith ("Found no solution after exhaustively searching the " ++ "dependency tree. Rerunning the dependency solver " @@ -147,17 +144,17 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = ExhaustiveSearch cs' cm' -> fromProgress $ Fail $ rerunSolverForErrorMsg cs' - ++ finalErrorMsg verbosity mbj (ExhaustiveSearch cs' cm') + ++ finalErrorMsg sc (ExhaustiveSearch cs' cm') BackjumpLimitReached -> fromProgress $ Fail $ "Reached backjump limit while trying to minimize the " ++ "conflict set to create a better error message. " ++ "Original error message:\n" ++ rerunSolverForErrorMsg cs - ++ finalErrorMsg verbosity mbj failure + ++ finalErrorMsg sc failure else fromProgress $ Fail $ - rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure - createErrorMsg verbosity mbj failure@BackjumpLimitReached = + rerunSolverForErrorMsg cs ++ finalErrorMsg sc failure + createErrorMsg failure@BackjumpLimitReached = continueWith ("Backjump limit reached. Rerunning dependency solver to generate " ++ "a final conflict set for the search tree containing the " @@ -166,11 +163,11 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = \case ExhaustiveSearch cs _ -> fromProgress $ Fail $ - rerunSolverForErrorMsg cs ++ finalErrorMsg verbosity mbj failure + rerunSolverForErrorMsg cs ++ finalErrorMsg sc failure BackjumpLimitReached -> -- This case is possible when the number of goals involved in -- conflicts is greater than the backjump limit. - fromProgress $ Fail $ finalErrorMsg verbosity mbj failure + fromProgress $ Fail $ finalErrorMsg sc failure ++ "Failed to generate a summarized dependency solver " ++ "log due to low backjump limit." @@ -316,19 +313,27 @@ toVar (PackageVar qpn) = P qpn toVar (FlagVar qpn fn) = F (FN qpn fn) toVar (StanzaVar qpn sn) = S (SN qpn sn) -finalErrorMsg :: Verbosity -> Maybe Int -> SolverFailure -> String -finalErrorMsg verbosity mbj failure = +finalErrorMsg :: SolverConfig -> SolverFailure -> String +finalErrorMsg sc failure = case failure of ExhaustiveSearch cs cm -> "After searching the rest of the dependency tree exhaustively, " ++ "these were the goals I've had most trouble fulfilling: " ++ showCS cm cs + ++ flagSuggestion where - showCS = if verbosity > normal + showCS = if solverVerbosity sc > normal then CS.showCSWithFrequency else CS.showCSSortedByFrequency + flagSuggestion = + -- Don't suggest --minimize-conflict-set if the conflict set is + -- already small, because it is unlikely to be reduced further. + if CS.size cs > 3 && not (asBool (minimizeConflictSet sc)) + then "\nTry running with --minimize-conflict-set to improve the " + ++ "error message." + else "" BackjumpLimitReached -> - "Backjump limit reached (" ++ currlimit mbj ++ + "Backjump limit reached (" ++ currlimit (maxBackjumps sc) ++ "change with --max-backjumps or try to run with --reorder-goals).\n" where currlimit (Just n) = "currently " ++ show n ++ ", " currlimit Nothing = "" diff --git a/cabal-install/Distribution/Solver/Modular/ConflictSet.hs b/cabal-install/Distribution/Solver/Modular/ConflictSet.hs index 6c8b47aa96a..28991e3da04 100644 --- a/cabal-install/Distribution/Solver/Modular/ConflictSet.hs +++ b/cabal-install/Distribution/Solver/Modular/ConflictSet.hs @@ -26,6 +26,7 @@ module Distribution.Solver.Modular.ConflictSet ( , delete , empty , singleton + , size , member , filter , fromList @@ -171,6 +172,9 @@ singleton var = CS { #endif } +size :: ConflictSet -> Int +size = S.size . conflictSetToSet + member :: Var QPN -> ConflictSet -> Bool member var = S.member var . conflictSetToSet From 3bb7f2733a5536cc0896999031b9e40dcd90f7b2 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sat, 17 Nov 2018 13:58:23 -0800 Subject: [PATCH 09/10] Add --minimize-conflict-set to changelog. --- cabal-install/changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cabal-install/changelog b/cabal-install/changelog index 0a8725a0ecd..cd54da944e3 100644 --- a/cabal-install/changelog +++ b/cabal-install/changelog @@ -6,6 +6,8 @@ * Improved error messages for cabal file parse errors. (#5710) * Removed support for `.zip` format source distributions (#5755) * Add "simple project" initialization option. (#5707) + * Add '--minimize-conflict-set' flag to try to improve the solver's + error message, but with an increase in run time. (#5647) 2.4.1.0 Mikhail Glushenkov November 2018 * Add message to alert user to potential package casing errors. (#5635) From b1d629e25924dc6ba99cf11324ef32a502cc92a3 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sat, 17 Nov 2018 13:58:23 -0800 Subject: [PATCH 10/10] Document --minimize-conflict-set. --- Cabal/doc/nix-local-build.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Cabal/doc/nix-local-build.rst b/Cabal/doc/nix-local-build.rst index 363c8245e70..c8584f87e32 100644 --- a/Cabal/doc/nix-local-build.rst +++ b/Cabal/doc/nix-local-build.rst @@ -2073,6 +2073,21 @@ Most users generally won't need these. The command line variant of this field is ``--(no-)count-conflicts``. +.. cfg-field:: minimize-conflict-set: boolean + --minimize-conflict-set + --no-minimize-conflict-set + :synopsis: Try to improve the solver error message when there is no + solution. + + :default: False + + When there is no solution, try to improve the solver error message + by finding a minimal conflict set. This option may increase run + time significantly, so it is off by default. + + The command line variant of this field is + ``--(no-)minimize-conflict-set``. + .. cfg-field:: strong-flags: boolean --strong-flags --no-strong-flags