{-# LANGUAGE PatternGuards, ViewPatterns, RecordWildCards, FlexibleContexts, ScopedTypeVariables #-} -- Keep until 'checkSide', 'checkDefine', ... are used. {-# OPTIONS_GHC -Wno-unused-top-binds #-} {- The matching does a fairly simple unification between the two terms, treating any single letter variable on the left as a free variable. After the matching we substitute, transform and check the side conditions. We also "see through" both ($) and (.) functions on the right. TRANSFORM PATTERNS _eval_ - perform deep evaluation, must be used at the top of a RHS _noParen_ - don't bracket this particular item SIDE CONDITIONS (&&), (||), not - boolean connectives isAtom x - does x never need brackets isFoo x - is the root constructor of x a "Foo" notEq x y - are x and y not equal notIn xs ys - are all x variables not in ys expressions noTypeCheck, noQuickCheck - no semantics, a hint for testing only ($) AND (.) We see through ($)/(.) by expanding it if nothing else matches. We also see through (.) by translating rules that have (.) equivalents to separate rules. For example: concat (map f x) ==> concatMap f x -- we spot both these rules can eta reduce with respect to x concat . map f ==> concatMap f -- we use the associativity of (.) to add concat . map f . x ==> concatMap f . x -- currently 36 of 169 rules have (.) equivalents We see through (.) if the RHS is dull using id, e.g. not (not x) ==> x not . not ==> id not . not . x ==> x -} module Hint.Match(readMatch,readMatch') where import Control.Applicative import Data.List.Extra import Data.Maybe import Config.Type import Hint.Type import Control.Monad import Data.Tuple.Extra import HSE.Unify import Util import Timing import qualified Data.Set as Set import Prelude import qualified Refact.Types as R import qualified HsSyn as GHC import qualified SrcLoc as GHC import qualified BasicTypes as GHC import RdrName import OccName import Data.Data import GHC.Util fmapAn :: Exp b -> Exp SrcSpanInfo fmapAn = fmap (const an) --------------------------------------------------------------------- -- READ THE RULE -- old readMatch :: [HintRule] -> DeclHint readMatch settings = findIdeas (concatMap readRule settings) -- new readMatch' :: [HintRule] -> Scope' -> ModuleEx -> GHC.LHsDecl GHC.GhcPs -> [Idea] readMatch' settings = findIdeas' (concatMap readRule' settings) -- old readRule :: HintRule -> [HintRule] readRule m@HintRule{hintRuleLHS=(fmapAn -> hintRuleLHS), hintRuleRHS=(fmapAn -> hintRuleRHS), hintRuleSide=(fmap fmapAn -> hintRuleSide)} = (:) m{hintRuleLHS=hintRuleLHS,hintRuleSide=hintRuleSide,hintRuleRHS=hintRuleRHS} $ do (l,v1) <- dotVersion hintRuleLHS (r,v2) <- dotVersion hintRuleRHS guard $ v1 == v2 && l /= [] && (length l > 1 || length r > 1) && Set.notMember v1 (freeVars $ maybeToList hintRuleSide ++ l ++ r) if r /= [] then [m{hintRuleLHS=dotApps l, hintRuleRHS=dotApps r, hintRuleSide=hintRuleSide} ,m{hintRuleLHS=dotApps (l++[toNamed v1]), hintRuleRHS=dotApps (r++[toNamed v1]), hintRuleSide=hintRuleSide}] else if length l > 1 then [m{hintRuleLHS=dotApps l, hintRuleRHS=toNamed "id", hintRuleSide=hintRuleSide} ,m{hintRuleLHS=dotApps (l++[toNamed v1]), hintRuleRHS=toNamed v1, hintRuleSide=hintRuleSide}] else [] -- new readRule' :: HintRule -> [HintRule] readRule' m@HintRule{ hintRuleGhcLHS=(stripLocs' . unwrap -> hintRuleGhcLHS) , hintRuleGhcRHS=(stripLocs' . unwrap -> hintRuleGhcRHS) , hintRuleGhcSide=((stripLocs' . unwrap <$>) -> hintRuleGhcSide) } = (:) m{ hintRuleGhcLHS=wrap hintRuleGhcLHS , hintRuleGhcRHS=wrap hintRuleGhcRHS , hintRuleGhcSide=wrap <$> hintRuleGhcSide } $ do (l, v1) <- dotVersion' hintRuleGhcLHS (r, v2) <- dotVersion' hintRuleGhcRHS guard $ v1 == v2 && not (null l) && (length l > 1 || length r > 1) && Set.notMember v1 (Set.map occNameString (freeVars' $ maybeToList hintRuleGhcSide ++ l ++ r)) if not (null r) then [ m{ hintRuleGhcLHS=wrap (dotApps' l), hintRuleGhcRHS=wrap (dotApps' r), hintRuleGhcSide=wrap <$> hintRuleGhcSide } , m{ hintRuleGhcLHS=wrap (dotApps' (l ++ [strToVar' v1])), hintRuleGhcRHS=wrap (dotApps' (r ++ [strToVar' v1])), hintRuleGhcSide=wrap <$> hintRuleGhcSide } ] else if length l > 1 then [ m{ hintRuleGhcLHS=wrap (dotApps' l), hintRuleGhcRHS=wrap (strToVar' "id"), hintRuleGhcSide=wrap <$> hintRuleGhcSide } , m{ hintRuleGhcLHS=wrap (dotApps' (l++[strToVar' v1])), hintRuleGhcRHS=wrap (strToVar' v1), hintRuleGhcSide=wrap <$> hintRuleGhcSide}] else [] -- old -- find a dot version of this rule, return the sequence of app prefixes, and the var dotVersion :: Exp_ -> [([Exp_], String)] dotVersion (view -> Var_ v) | isUnifyVar v = [([], v)] dotVersion (App l ls rs) = first (ls :) <$> dotVersion (fromParen rs) dotVersion (InfixApp l x op y) = (first (LeftSection l x op :) <$> dotVersion y) ++ (first (RightSection l op y:) <$> dotVersion x) dotVersion _ = [] -- new -- Find a dot version of this rule, return the sequence of app -- prefixes, and the var. dotVersion' :: GHC.LHsExpr GHC.GhcPs -> [([GHC.LHsExpr GHC.GhcPs], String)] dotVersion' (view' -> Var_' v) | isUnifyVar v = [([], v)] dotVersion' (GHC.LL _ (GHC.HsApp _ ls rs)) = first (ls :) <$> dotVersion' (fromParen' rs) dotVersion' (GHC.LL l (GHC.OpApp _ x op y)) =(first (GHC.cL l (GHC.SectionL GHC.noExt x op) :) <$> dotVersion' y) ++ (first (GHC.cL l (GHC.SectionR GHC.noExt op y) :) <$> dotVersion' x) dotVersion' _ = [] --------------------------------------------------------------------- -- PERFORM THE MATCHING -- old findIdeas :: [HintRule] -> Scope -> ModuleEx -> Decl_ -> [Idea] findIdeas matches s _ decl = timed "Hint" "Match apply" $ forceList [ (idea (hintRuleSeverity m) (hintRuleName m) x y [r]){ideaNote=notes} | decl <- findDecls decl , (parent,x) <- universeParentExp decl , m <- matches, Just (y,notes, subst) <- [matchIdea s decl m parent x] , let r = R.Replace R.Expr (toSS x) subst (prettyPrint $ hintRuleRHS m) ] -- new findIdeas' :: [HintRule] -> Scope' -> ModuleEx -> GHC.LHsDecl GHC.GhcPs -> [Idea] findIdeas' matches s _ decl = timed "Hint" "Match apply" $ forceList [ (idea' (hintRuleSeverity m) (hintRuleName m) x y [r]){ideaNote=notes} | decl <- findDecls' decl , (parent,x) <- universeParentExp' decl , m <- matches, Just (y, notes, subst) <- [matchIdea' s decl m parent x] , let r = R.Replace R.Expr (toSS' x) subst (unsafePrettyPrint $ unwrap (hintRuleGhcRHS m)) ] -- old findDecls :: Decl_ -> [Decl_] findDecls x@InstDecl{} = children x findDecls RulePragmaDecl{} = [] -- often rules contain things that HLint would rewrite findDecls x = [x] -- new findDecls' :: GHC.LHsDecl GHC.GhcPs -> [GHC.LHsDecl GHC.GhcPs] findDecls' x@(GHC.LL _ GHC.InstD{}) = children x findDecls' (GHC.LL _ GHC.RuleD{}) = [] -- Often rules contain things that HLint would rewrite. findDecls' x = [x] -- old matchIdea :: Scope -> Decl_ -> HintRule -> Maybe (Int, Exp_) -> Exp_ -> Maybe (Exp_, [Note], [(String, R.SrcSpan)]) matchIdea s decl HintRule{..} parent x = do let nm a b = scopeMatch (hintRuleScope,a) (s,b) u <- unifyExp nm True hintRuleLHS x u <- validSubst (=~=) u -- need to check free vars before unqualification, but after subst (with e) -- need to unqualify before substitution (with res) let e = substitute u hintRuleRHS res = addBracket parent $ performSpecial $ substitute u $ unqualify hintRuleScope s hintRuleRHS guard $ (freeVars e Set.\\ Set.filter (not . isUnifyVar) (freeVars hintRuleRHS)) `Set.isSubsetOf` freeVars x -- check no unexpected new free variables -- check it isn't going to get broken by QuasiQuotes as per #483 -- if we have lambdas we might be moving, and QuasiQuotes, we might inadvertantly break free vars -- because quasi quotes don't show what free vars they make use of guard $ not (any isLambda $ universe hintRuleLHS) || not (any isQuasiQuote $ universe x) guard $ checkSide hintRuleSide $ ("original",x) : ("result",res) : fromSubst u guard $ checkDefine decl parent res return (res, hintRuleNotes, [(s, toSS pos) | (s, pos) <- fromSubst u, ann pos /= an]) -- new matchIdea' :: Scope' -> GHC.LHsDecl GHC.GhcPs -> HintRule -> Maybe (Int, GHC.LHsExpr GHC.GhcPs) -> GHC.LHsExpr GHC.GhcPs -> Maybe (GHC.LHsExpr GHC.GhcPs, [Note], [(String, R.SrcSpan)]) matchIdea' sb decl HintRule{..} parent x = do let lhs = unwrap hintRuleGhcLHS rhs = unwrap hintRuleGhcRHS sa = unwrap hintRuleGhcScope nm a b = scopeMatch' (sa, a) (sb, b) u <- unifyExp' nm True lhs x u <- validSubst' eqNoLoc' u -- Need to check free vars before unqualification, but after subst -- (with 'e') need to unqualify before substitution (with 'res'). let e = substitute' u rhs res = addBracket' parent $ performSpecial' $ substitute' u $ unqualify' sa sb rhs guard $ (freeVars' e Set.\\ Set.filter (not . isUnifyVar . occNameString) (freeVars' rhs)) `Set.isSubsetOf` freeVars' x -- Check no unexpected new free variables. -- Check it isn't going to get broken by QuasiQuotes as per #483. If -- we have lambdas we might be moving, and QuasiQuotes, we might -- inadvertantly break free vars because quasi quotes don't show -- what free vars they make use of. guard $ not (any isLambda' $ universe lhs) || not (any isQuasiQuote' $ universe x) guard $ checkSide' (unwrap <$> hintRuleGhcSide) $ ("original", x) : ("result", res) : fromSubst' u guard $ checkDefine' decl parent res return (res, hintRuleNotes, [(s, toSS' pos) | (s, pos) <- fromSubst' u, GHC.getLoc pos /= GHC.noSrcSpan]) --------------------------------------------------------------------- -- SIDE CONDITIONS -- old checkSide :: Maybe Exp_ -> [(String, Exp_)] -> Bool checkSide x bind = maybe True bool x where bool :: Exp_ -> Bool bool (InfixApp _ x op y) | opExp op ~= "&&" = bool x && bool y | opExp op ~= "||" = bool x || bool y | opExp op ~= "==" = expr (fromParen1 x) =~= expr (fromParen1 y) bool (App _ x y) | x ~= "not" = not $ bool y bool (Paren _ x) = bool x bool (App _ cond (sub -> y)) | 'i':'s':typ <- fromNamed cond = isType typ y bool (App _ (App _ cond (sub -> x)) (sub -> y)) | cond ~= "notIn" = and [x `notElem` universe y | x <- list x, y <- list y] | cond ~= "notEq" = x /=~= y bool x | x ~= "noTypeCheck" = True bool x | x ~= "noQuickCheck" = True bool x = error $ "Hint.Match.checkSide, unknown side condition: " ++ prettyPrint x expr :: Exp_ -> Exp_ expr (App _ (fromNamed -> "subst") x) = sub $ fromParen1 x expr x = x isType "Compare" x = True -- just a hint for proof stuff isType "Atom" x = isAtom x isType "WHNF" x = isWHNF x isType "Wildcard" x = any isFieldWildcard $ universeS x isType "Nat" (asInt -> Just x) | x >= 0 = True isType "Pos" (asInt -> Just x) | x > 0 = True isType "Neg" (asInt -> Just x) | x < 0 = True isType "NegZero" (asInt -> Just x) | x <= 0 = True isType ('L':'i':'t':typ@(_:_)) (Lit _ x) = -- This case only comes up in the tests with 'LitInt'. let top = head (words $ show x) in typ == top isType typ x = let top = head (words $ show x) in typ == top asInt :: Exp_ -> Maybe Integer asInt (Paren _ x) = asInt x asInt (NegApp _ x) = negate <$> asInt x asInt (Lit _ (Int _ x _)) = Just x asInt _ = Nothing list :: Exp_ -> [Exp_] list (List _ xs) = xs list x = [x] sub :: Exp_ -> Exp_ sub = transform f where f (view -> Var_ x) | Just y <- lookup x bind = y f x = x -- new checkSide' :: Maybe (GHC.LHsExpr GHC.GhcPs) -> [(String, GHC.LHsExpr GHC.GhcPs)] -> Bool checkSide' x bind = maybe True bool x where bool :: GHC.LHsExpr GHC.GhcPs -> Bool bool (GHC.LL _ (GHC.OpApp _ x op y)) | varToStr' op == "&&" = bool x && bool y | varToStr' op == "||" = bool x || bool y | varToStr' op == "==" = expr (fromParen1' x) `eqNoLoc'` expr (fromParen1' y) bool (GHC.LL _ (GHC.HsApp _ x y)) | varToStr' x == "not" = not $ bool y bool (GHC.LL _ (GHC.HsPar _ x)) = bool x bool (GHC.LL _ (GHC.HsApp _ cond (sub -> y))) | 'i' : 's' : typ <- varToStr' cond = isType typ y bool (GHC.LL _ (GHC.HsApp _ (GHC.LL _ (GHC.HsApp _ cond (sub -> x))) (sub -> y))) | varToStr' cond == "notIn" = and [wrap (stripLocs' x) `notElem` map (wrap . stripLocs') (universe y) | x <- list x, y <- list y] | varToStr' cond == "notEq" = not (x `eqNoLoc'` y) bool x | varToStr' x == "noTypeCheck" = True bool x | varToStr' x == "noQuickCheck" = True bool x = error $ "Hint.Match.checkSide', unknown side condition: " ++ unsafePrettyPrint x expr :: GHC.LHsExpr GHC.GhcPs -> GHC.LHsExpr GHC.GhcPs expr (GHC.LL _ (GHC.HsApp _ (varToStr' -> "subst") x)) = sub $ fromParen1' x expr x = x isType "Compare" x = True -- Just a hint for proof stuff isType "Atom" x = isAtom' x isType "WHNF" x = isWHNF' x isType "Wildcard" x = any isFieldPun' (universeBi x) || any hasFieldsDotDot' (universeBi x) isType "Nat" (asInt -> Just x) | x >= 0 = True isType "Pos" (asInt -> Just x) | x > 0 = True isType "Neg" (asInt -> Just x) | x < 0 = True isType "NegZero" (asInt -> Just x) | x <= 0 = True isType "LitInt" (GHC.LL _ (GHC.HsLit _ GHC.HsInt{})) = True isType "LitInt" (GHC.LL _ (GHC.HsOverLit _ (GHC.OverLit _ GHC.HsIntegral{} _))) = True isType "Var" (GHC.LL _ GHC.HsVar{}) = True isType "App" (GHC.LL _ GHC.HsApp{}) = True isType "InfixApp" (GHC.LL _ x@GHC.OpApp{}) = True isType "Paren" (GHC.LL _ x@GHC.HsPar{}) = True isType "Tuple" (GHC.LL _ GHC.ExplicitTuple{}) = True isType typ (GHC.LL _ x) = let top = showConstr (toConstr x) in typ == top isType _ _ = False -- {-# COMPLETE LL#-} asInt :: GHC.LHsExpr GHC.GhcPs -> Maybe Integer asInt (GHC.LL _ (GHC.HsPar _ x)) = asInt x asInt (GHC.LL _ (GHC.NegApp _ x _)) = negate <$> asInt x asInt (GHC.LL _ (GHC.HsLit _ (GHC.HsInt _ (GHC.IL _ neg x)) )) = Just $ if neg then -x else x asInt (GHC.LL _ (GHC.HsOverLit _ (GHC.OverLit _ (GHC.HsIntegral (GHC.IL _ neg x)) _))) = Just $ if neg then -x else x asInt _ = Nothing list :: GHC.LHsExpr GHC.GhcPs -> [GHC.LHsExpr GHC.GhcPs] list (GHC.LL _ (GHC.ExplicitList _ _ xs)) = xs list x = [x] sub :: GHC.LHsExpr GHC.GhcPs -> GHC.LHsExpr GHC.GhcPs sub = transform f where f (view' -> Var_' x) | Just y <- lookup x bind = y f x = x -- old -- does the result look very much like the declaration checkDefine :: Decl_ -> Maybe (Int, Exp_) -> Exp_ -> Bool checkDefine x Nothing y = fromNamed x /= fromNamed (transformBi unqual $ head $ fromApps y) checkDefine _ _ _ = True -- new -- Does the result look very much like the declaration? checkDefine' :: GHC.LHsDecl GHC.GhcPs -> Maybe (Int, GHC.LHsExpr GHC.GhcPs) -> GHC.LHsExpr GHC.GhcPs -> Bool checkDefine' x Nothing y = declName x /= Just (varToStr' (transformBi unqual' $ head $ fromApps' y)) checkDefine' _ _ _ = True --------------------------------------------------------------------- -- TRANSFORMATION -- old -- if it has _eval_ do evaluation on it performSpecial :: Exp_ -> Exp_ performSpecial = transform fNoParen . fEval where fEval (App _ e x) | e ~= "_eval_" = reduce x fEval x = x fNoParen (App _ e x) | e ~= "_noParen_" = fromParen x fNoParen x = x -- new -- If it has '_eval_' do evaluation on it. performSpecial' :: GHC.LHsExpr GHC.GhcPs -> GHC.LHsExpr GHC.GhcPs performSpecial' = transform fNoParen . fEval where fEval, fNoParen :: GHC.LHsExpr GHC.GhcPs -> GHC.LHsExpr GHC.GhcPs fEval (GHC.LL _ (GHC.HsApp _ e x)) | varToStr' e == "_eval_" = reduce' x fEval x = x fNoParen (GHC.LL _ (GHC.HsApp _ e x)) | varToStr' e == "_noParen_" = fromParen' x fNoParen x = x -- old -- contract Data.List.foo ==> foo, if Data.List is loaded unqualify :: Scope -> Scope -> Exp_ -> Exp_ unqualify from to = transformBi f where f x@(UnQual _ (Ident _ s)) | isUnifyVar s = x f x = scopeMove (from,x) to -- new -- Contract : 'Data.List.foo' => 'foo' if 'Data.List' is loaded. unqualify' :: Scope' -> Scope' -> GHC.LHsExpr GHC.GhcPs -> GHC.LHsExpr GHC.GhcPs unqualify' from to = transformBi f where f :: GHC.Located RdrName -> GHC.Located RdrName f x@(GHC.L _ (Unqual s)) | isUnifyVar (occNameString s) = x f x = scopeMove' (from, x) to -- old addBracket :: Maybe (Int,Exp_) -> Exp_ -> Exp_ addBracket (Just (i,p)) c | needBracketOld i p c = Paren an c addBracket _ x = x -- new addBracket' :: Maybe (Int, GHC.LHsExpr GHC.GhcPs) -> GHC.LHsExpr GHC.GhcPs -> GHC.LHsExpr GHC.GhcPs addBracket' (Just (i, p)) c | needBracketOld' i p c = GHC.noLoc $ GHC.HsPar GHC.noExt c addBracket' _ x = x