{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE ViewPatterns #-} -- | -- Copyright : (c) 2010-2012 Simon Meier & Benedikt Schmidt -- License : GPL v3 (see LICENSE) -- -- Maintainer : Simon Meier -- Portability : GHC only -- -- Proof methods and heuristics: the external small-step interface to the -- constraint solver. module Theory.Constraint.Solver.ProofMethod ( -- * Proof methods CaseName , ProofMethod(..) , execProofMethod -- ** Heuristics , GoalRanking(..) , goalRankingName , rankProofMethods , Heuristic , roundRobinHeuristic , useHeuristic -- ** Pretty Printing , prettyProofMethod ) where import Data.Binary import Data.DeriveTH import Data.Function (on) import Data.Label hiding (get) import qualified Data.Label as L import Data.List import qualified Data.Map as M import Data.Monoid import Data.Ord (comparing) import qualified Data.Set as S import Extension.Prelude (sortOn) import Control.Basics import Control.DeepSeq import Control.Monad.Bind import qualified Control.Monad.Trans.PreciseFresh as Precise import Theory.Constraint.Solver.CaseDistinctions import Theory.Constraint.Solver.Contradictions import Theory.Constraint.Solver.Goals import Theory.Constraint.Solver.Reduction import Theory.Constraint.Solver.Simplify import Theory.Constraint.Solver.Types import Theory.Constraint.System import Theory.Model import Theory.Text.Pretty ------------------------------------------------------------------------------ -- Utilities ------------------------------------------------------------------------------ -- | @uniqueListBy eq changes xs@ zips the @changes@ with all sequences equal -- elements in the list. -- -- > uniqueListBy compare id (const [ (++ show i) | i <- [1..] ]) ["a","b","a"] = -- > ["a1","b","a2"] -- uniqueListBy :: (a -> a -> Ordering) -> (a -> a) -> (Int -> [a -> a]) -> [a] -> [a] uniqueListBy ord single distinguish xs0 = map fst $ sortBy (comparing snd) $ concat $ map uniquify $ groupBy (\x y -> ord (fst x) (fst y) == EQ) $ sortBy (ord `on` fst) $ zip xs0 [(0::Int)..] where uniquify [] = error "impossible" uniquify [(x,i)] = [(single x, i)] uniquify xs = zipWith (\f (x,i) -> (f x, i)) dist xs where dist = distinguish $ length xs ------------------------------------------------------------------------------ -- Proof Methods ------------------------------------------------------------------------------ -- | Every case in a proof is uniquely named. type CaseName = String -- | Sound transformations of sequents. data ProofMethod = Sorry (Maybe String) -- ^ Proof was not completed | Solved -- ^ An attack was fond | Simplify -- ^ A simplification step. | SolveGoal Goal -- ^ A goal that was solved. | Contradiction (Maybe Contradiction) -- ^ A contradiction could be -- derived, possibly with a reason. | Induction -- ^ Use inductive strengthening on -- the single formula constraint in -- the system. deriving( Eq, Ord, Show ) instance HasFrees ProofMethod where foldFrees f (SolveGoal g) = foldFrees f g foldFrees f (Contradiction c) = foldFrees f c foldFrees _ _ = mempty mapFrees f (SolveGoal g) = SolveGoal <$> mapFrees f g mapFrees f (Contradiction c) = Contradiction <$> mapFrees f c mapFrees _ method = pure method -- Proof method execution ------------------------- -- @execMethod rules method se@ checks first if the @method@ is applicable to -- the sequent @se@. Then, it applies the @method@ to the sequent under the -- assumption that the @rules@ describe all rewriting rules in scope. -- -- NOTE that the returned systems have their free substitution fully applied -- and all variable indices reset. execProofMethod :: ProofContext -> ProofMethod -> System -> Maybe (M.Map CaseName System) execProofMethod ctxt method sys = M.map cleanupSystem <$> case method of Sorry _ -> return M.empty Solved | null (openGoals sys) -> return M.empty | otherwise -> Nothing SolveGoal goal | goal `M.member` L.get sGoals sys -> execSolveGoal goal | otherwise -> Nothing Simplify -> singleCase (/=) simplifySystem Induction -> execInduction Contradiction _ | null (contradictions ctxt sys) -> Nothing | otherwise -> Just M.empty where -- at this point it is safe to remove the free substitution, as all -- systems have it fully applied (by the virtue of a call to -- simplifySystem). We also reset the variable indices here. cleanupSystem = (`Precise.evalFresh` Precise.nothingUsed) . (`evalBindT` noBindings) . someInst . set sSubst emptySubst -- expect only one or no subcase in the given case distinction singleCase check m = case map fst $ getDisj $ execReduction m ctxt sys (avoid sys) of [] -> return $ M.empty [sys'] | check sys sys' -> return $ M.singleton "" sys' | otherwise -> mzero syss -> return $ M.fromList (zip (map show [(1::Int)..]) syss) -- solve the given goal -- PRE: Goal must be valid in this system. execSolveGoal goal = do return $ makeCaseNames $ map fst $ getDisj $ runReduction solver ctxt sys (avoid sys) where ths = L.get pcCaseDists ctxt solver = do name <- maybe (solveGoal goal) (fmap $ concat . intersperse "_") (solveWithCaseDistinction ctxt ths goal) simplifySystem return name makeCaseNames = M.fromListWith (error "case names not unique") . uniqueListBy (comparing fst) id distinguish where distinguish n = [ (\(x,y) -> (x ++ "_case_" ++ pad (show i), y)) | i <- [(1::Int)..] ] where l = length (show n) pad cs = replicate (l - length cs) '0' ++ cs -- Apply induction: possible if the system contains only -- a single, last-free, closed formula. execInduction | sys == sys0 = case S.toList $ L.get sFormulas sys of [gf] -> case ginduct gf of Right (bc, sc) -> Just $ insCase "empty_trace" bc $ insCase "non_empty_trace" sc $ M.empty _ -> Nothing _ -> Nothing | otherwise = Nothing where sys0 = set sFormulas (L.get sFormulas sys) $ set sLemmas (L.get sLemmas sys) $ emptySystem (L.get sCaseDistKind sys) insCase name gf = M.insert name (set sFormulas (S.singleton gf) sys) ------------------------------------------------------------------------------ -- Heuristics ------------------------------------------------------------------------------ -- | The different available functions to rank goals with respect to their -- order of solving in a constraint system. data GoalRanking = GoalNrRanking | UsefulGoalNrRanking | SmartRanking Bool deriving( Eq, Ord, Show ) -- | The name/explanation of a 'GoalRanking'. goalRankingName :: GoalRanking -> String goalRankingName ranking = "Goals sorted according to " ++ case ranking of GoalNrRanking -> "their order of creation" UsefulGoalNrRanking -> "their usefulness and order of creation" SmartRanking useLoopBreakers -> smart useLoopBreakers where smart b = "the 'smart' heuristic (loop breakers " ++ (if b then "allowed" else "delayed") ++ ")." -- | Use a 'GoalRanking' to sort a list of 'AnnotatedGoal's stemming from the -- given constraint 'System'. rankGoals :: GoalRanking -> System -> [AnnotatedGoal] -> [AnnotatedGoal] rankGoals ranking = case ranking of GoalNrRanking -> \_sys -> goalNrRanking UsefulGoalNrRanking -> \_sys -> sortOn (\(_, (nr, useless)) -> (useless, nr)) SmartRanking useLoopsBreakers -> smartRanking useLoopsBreakers -- | Use a 'GoalRanking' to generate the ranked, list of possible -- 'ProofMethod's and their corresponding results in this 'ProofContext' and -- for this 'System'. If the resulting list is empty, then the constraint -- system is solved. rankProofMethods :: GoalRanking -> ProofContext -> System -> [(ProofMethod, (M.Map CaseName System, String))] rankProofMethods ranking ctxt sys = do (m, expl) <- (contradiction <$> contradictions ctxt sys) <|> (case L.get pcUseInduction ctxt of AvoidInduction -> [(Simplify, ""), (Induction, "")] UseInduction -> [(Induction, ""), (Simplify, "")] ) <|> (solveGoalMethod <$> (rankGoals ranking sys $ openGoals sys)) case execProofMethod ctxt m sys of Just cases -> return (m, (cases, expl)) Nothing -> [] where contradiction c = (Contradiction (Just c), "") solveGoalMethod (goal, (nr, usefulness)) = ( SolveGoal goal , "nr. " ++ show nr ++ case usefulness of Useful -> "" LoopBreaker -> " (loop breaker)" ProbablyConstructible -> " (probably constructible)" CurrentlyDeducible -> " (currently deducible)" ) newtype Heuristic = Heuristic [GoalRanking] deriving( Eq, Ord, Show ) -- | Smart constructor for heuristics. Schedules the goal rankings in a -- round-robin fashion dependent on the proof depth. roundRobinHeuristic :: [GoalRanking] -> Heuristic roundRobinHeuristic = Heuristic -- | Use a heuristic to schedule a 'GoalRanking' according to the given -- proof-depth. useHeuristic :: Heuristic -> Int -> GoalRanking useHeuristic (Heuristic [] ) = error "useHeuristic: empty list of rankings" useHeuristic (Heuristic rankings) = ranking where n = length rankings ranking depth | depth < 0 = error $ "useHeuristic: negative proof depth " ++ show depth | otherwise = rankings !! (depth `mod` n) {- -- | Schedule the given local-heuristics in a round-robin fashion. roundRobinHeuristic :: [GoalRanking] -> Heuristic roundRobinHeuristic [] = error "roundRobin: empty list of rankings" roundRobinHeuristic rankings = methods where n = length rankings methods depth ctxt sys | depth < 0 = error $ "roundRobin: negative proof depth " ++ show depth | otherwise = ( name , ((Contradiction . Just) <$> contradictions ctxt sys) <|> (case L.get pcUseInduction ctxt of AvoidInduction -> [Simplify, Induction] UseInduction -> [Induction, Simplify] ) <|> ((SolveGoal . fst) <$> (ranking sys $ openGoals sys)) ) where (name, ranking) = rankings !! (depth `mod` n) -} -- | Sort annotated goals according to their number. goalNrRanking :: [AnnotatedGoal] -> [AnnotatedGoal] goalNrRanking = sortOn (fst . snd) -- | A ranking function tuned for the automatic verification of -- classical security protocols that exhibit a well-founded protocol premise -- fact flow. smartRanking :: Bool -- True if PremiseG loop-breakers should not be delayed -> System -> [AnnotatedGoal] -> [AnnotatedGoal] smartRanking allowPremiseGLoopBreakers sys = sortOnUsefulness . unmark . sortDecisionTree solveFirst . goalNrRanking where sortOnUsefulness = sortOn (tagUsefulness . snd . snd) tagUsefulness Useful = 0 :: Int tagUsefulness ProbablyConstructible = 1 tagUsefulness LoopBreaker = 1 tagUsefulness CurrentlyDeducible = 2 unmark | allowPremiseGLoopBreakers = map unmarkPremiseG | otherwise = id unmarkPremiseG (goal@(PremiseG _ _), (nr, _)) = (goal, (nr, Useful)) unmarkPremiseG annGoal = annGoal solveFirst = [ isChainGoal . fst , isDisjGoal . fst , isNonLoopBreakerProtoFactGoal , isStandardActionGoal . fst , isFreshKnowsGoal . fst , isSplitGoalSmall . fst , isDoubleExpGoal . fst , isNoLargeSplitGoal . fst ] -- move the rest (mostly more expensive KU-goals) before expensive -- equation splits -- FIXME: This small split goal preferral is quite hacky when using -- induction. The problem is that we may end up solving message premise -- goals all the time instead performing a necessary split. We should make -- sure that a split does not get too old. smallSplitGoalSize = 3 isNonLoopBreakerProtoFactGoal (PremiseG _ fa, (_, Useful)) = not $ isKFact fa isNonLoopBreakerProtoFactGoal _ = False msgPremise (ActionG _ fa) = do (UpK, m) <- kFactView fa; return m msgPremise _ = Nothing isFreshKnowsGoal goal = case msgPremise goal of Just (viewTerm -> Lit (Var lv)) | lvarSort lv == LSortFresh -> True _ -> False isDoubleExpGoal goal = case msgPremise goal of Just (viewTerm2 -> FExp _ (viewTerm2 -> FMult _)) -> True _ -> False -- Be conservative on splits that don't exist. isSplitGoalSmall (SplitG sid) = maybe False (<= smallSplitGoalSize) $ splitSize (L.get sEqStore sys) sid isSplitGoalSmall _ = False isNoLargeSplitGoal goal@(SplitG _) = isSplitGoalSmall goal isNoLargeSplitGoal _ = True -- | @sortDecisionTree xs ps@ returns a reordering of @xs@ -- such that the sublist satisfying @ps!!0@ occurs first, -- then the sublist satisfying @ps!!1@, and so on. sortDecisionTree :: [a -> Bool] -> [a] -> [a] sortDecisionTree [] xs = xs sortDecisionTree (p:ps) xs = sat ++ sortDecisionTree ps nonsat where (sat, nonsat) = partition p xs ------------------------------------------------------------------------------ -- Pretty printing ------------------------------------------------------------------------------ -- | Pretty-print a proof method. prettyProofMethod :: HighlightDocument d => ProofMethod -> d prettyProofMethod method = case method of Solved -> keyword_ "SOLVED" <-> lineComment_ "trace found" Induction -> keyword_ "induction" Sorry reason -> fsep [keyword_ "sorry", maybe emptyDoc lineComment_ reason] SolveGoal goal -> keyword_ "solve(" <-> prettyGoal goal <-> keyword_ ")" Simplify -> keyword_ "simplify" Contradiction reason -> sep [ keyword_ "contradiction" , maybe emptyDoc (lineComment . prettyContradiction) reason ] -- Derived instances -------------------- $( derive makeBinary ''ProofMethod) $( derive makeBinary ''GoalRanking) $( derive makeBinary ''Heuristic) $( derive makeNFData ''ProofMethod) $( derive makeNFData ''GoalRanking) $( derive makeNFData ''Heuristic)