diff options
-rw-r--r-- | compiler/ghc.cabal.in | 1 | ||||
-rw-r--r-- | compiler/main/Plugins.hs | 14 | ||||
-rw-r--r-- | compiler/typecheck/TcHoleErrors.hs | 282 | ||||
-rw-r--r-- | compiler/typecheck/TcHoleFitTypes.hs | 144 | ||||
-rw-r--r-- | compiler/typecheck/TcHoleFitTypes.hs-boot | 10 | ||||
-rw-r--r-- | compiler/typecheck/TcRnDriver.hs | 31 | ||||
-rw-r--r-- | compiler/typecheck/TcRnMonad.hs | 1 | ||||
-rw-r--r-- | compiler/typecheck/TcRnTypes.hs | 4 | ||||
-rw-r--r-- | docs/users_guide/extending_ghc.rst | 323 | ||||
-rw-r--r-- | testsuite/tests/plugins/Makefile | 4 | ||||
-rw-r--r-- | testsuite/tests/plugins/all.T | 8 | ||||
-rw-r--r-- | testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.cabal | 11 | ||||
-rw-r--r-- | testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.hs | 89 | ||||
-rw-r--r-- | testsuite/tests/plugins/hole-fit-plugin/Makefile | 18 | ||||
-rw-r--r-- | testsuite/tests/plugins/hole-fit-plugin/Setup.hs | 3 | ||||
-rw-r--r-- | testsuite/tests/plugins/test-hole-plugin.hs | 19 | ||||
-rw-r--r-- | testsuite/tests/plugins/test-hole-plugin.stderr | 66 |
17 files changed, 864 insertions, 164 deletions
diff --git a/compiler/ghc.cabal.in b/compiler/ghc.cabal.in index 1a235c4008..35810cc7c5 100644 --- a/compiler/ghc.cabal.in +++ b/compiler/ghc.cabal.in @@ -521,6 +521,7 @@ Library TcRules TcSimplify TcHoleErrors + TcHoleFitTypes TcErrors TcTyClsDecls TcTyDecls diff --git a/compiler/main/Plugins.hs b/compiler/main/Plugins.hs index 93297522db..26bd41fd08 100644 --- a/compiler/main/Plugins.hs +++ b/compiler/main/Plugins.hs @@ -30,6 +30,10 @@ module Plugins ( -- - access to loaded interface files with 'interfaceLoadAction' -- , keepRenamedSource + -- ** Hole fit plugins + -- | hole fit plugins allow plugins to change the behavior of valid hole + -- fit suggestions + , HoleFitPluginR -- * Internal , PluginWithArgs(..), plugins, pluginRecompile' @@ -42,7 +46,8 @@ import GhcPrelude import {-# SOURCE #-} CoreMonad ( CoreToDo, CoreM ) import qualified TcRnTypes -import TcRnTypes ( TcGblEnv, IfM, TcM, tcg_rn_decls, tcg_rn_exports ) +import TcRnTypes ( TcGblEnv, IfM, TcM, tcg_rn_decls, tcg_rn_exports ) +import TcHoleFitTypes ( HoleFitPluginR ) import HsSyn import DynFlags import HscTypes @@ -79,6 +84,9 @@ data Plugin = Plugin { , tcPlugin :: TcPlugin -- ^ An optional typechecker plugin, which may modify the -- behaviour of the constraint solver. + , holeFitPlugin :: HoleFitPlugin + -- ^ An optional plugin to handle hole fits, which may re-order + -- or change the list of valid hole fits and refinement hole fits. , pluginRecompile :: [CommandLineOption] -> IO PluginRecompile -- ^ Specify how the plugin should affect recompilation. , parsedResultAction :: [CommandLineOption] -> ModSummary -> HsParsedModule @@ -169,6 +177,7 @@ instance Monoid PluginRecompile where type CorePlugin = [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo] type TcPlugin = [CommandLineOption] -> Maybe TcRnTypes.TcPlugin +type HoleFitPlugin = [CommandLineOption] -> Maybe HoleFitPluginR purePlugin, impurePlugin, flagRecompile :: [CommandLineOption] -> IO PluginRecompile purePlugin _args = return NoForceRecompile @@ -186,7 +195,8 @@ defaultPlugin :: Plugin defaultPlugin = Plugin { installCoreToDos = const return , tcPlugin = const Nothing - , pluginRecompile = impurePlugin + , holeFitPlugin = const Nothing + , pluginRecompile = impurePlugin , renamedResultAction = \_ env grp -> return (env, grp) , parsedResultAction = \_ _ -> return , typeCheckResultAction = \_ _ -> return diff --git a/compiler/typecheck/TcHoleErrors.hs b/compiler/typecheck/TcHoleErrors.hs index a5a4cf28d4..8c9cf0285b 100644 --- a/compiler/typecheck/TcHoleErrors.hs +++ b/compiler/typecheck/TcHoleErrors.hs @@ -1,6 +1,18 @@ -module TcHoleErrors ( findValidHoleFits, tcFilterHoleFits, HoleFit (..) - , HoleFitCandidate (..), tcCheckHoleFit, tcSubsumes - , withoutUnification ) where +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ExistentialQuantification #-} +module TcHoleErrors ( findValidHoleFits, tcFilterHoleFits + , tcCheckHoleFit, tcSubsumes + , withoutUnification + , fromPureHFPlugin + -- Re-exports for convenience + , hfIsLcl + , pprHoleFit, debugHoleFitDispConfig + + -- Re-exported from TcHoleFitTypes + , TypedHole (..), HoleFit (..), HoleFitCandidate (..) + , CandPlugin, FitPlugin + , HoleFitPlugin (..), HoleFitPluginR (..) + ) where import GhcPrelude @@ -28,10 +40,9 @@ import FV ( fvVarList, fvVarSet, unionFV, mkFVs, FV ) import Control.Arrow ( (&&&) ) -import Control.Monad ( filterM, replicateM ) +import Control.Monad ( filterM, replicateM, foldM ) import Data.List ( partition, sort, sortOn, nubBy ) import Data.Graph ( graphFromEdges, topSort ) -import Data.Function ( on ) import TcSimplify ( simpl_top, runTcSDeriveds ) @@ -39,12 +50,14 @@ import TcUnify ( tcSubType_NC ) import ExtractDocs ( extractDocs ) import qualified Data.Map as Map -import HsDoc ( HsDocString, unpackHDS, DeclDocMap(..) ) +import HsDoc ( unpackHDS, DeclDocMap(..) ) import HscTypes ( ModIface(..) ) import LoadIface ( loadInterfaceForNameMaybe ) import PrelInfo (knownKeyNames) +import TcHoleFitTypes + {- Note [Valid hole fits include ...] @@ -420,72 +433,6 @@ getSortingAlg = then BySize else NoSorting } - --- | HoleFitCandidates are passed to the filter and checked whether they can be --- made to fit. -data HoleFitCandidate = IdHFCand Id -- An id, like locals. - | NameHFCand Name -- A name, like built-in syntax. - | GreHFCand GlobalRdrElt -- A global, like imported ids. - deriving (Eq) -instance Outputable HoleFitCandidate where - ppr = pprHoleFitCand - -pprHoleFitCand :: HoleFitCandidate -> SDoc -pprHoleFitCand (IdHFCand id) = text "Id HFC: " <> ppr id -pprHoleFitCand (NameHFCand name) = text "Name HFC: " <> ppr name -pprHoleFitCand (GreHFCand gre) = text "Gre HFC: " <> ppr gre - -instance HasOccName HoleFitCandidate where - occName hfc = case hfc of - IdHFCand id -> occName id - NameHFCand name -> occName name - GreHFCand gre -> occName (gre_name gre) - --- | HoleFit is the type we use for valid hole fits. It contains the --- element that was checked, the Id of that element as found by `tcLookup`, --- and the refinement level of the fit, which is the number of extra argument --- holes that this fit uses (e.g. if hfRefLvl is 2, the fit is for `Id _ _`). -data HoleFit = - HoleFit { hfId :: Id -- The elements id in the TcM - , hfCand :: HoleFitCandidate -- The candidate that was checked. - , hfType :: TcType -- The type of the id, possibly zonked. - , hfRefLvl :: Int -- The number of holes in this fit. - , hfWrap :: [TcType] -- The wrapper for the match. - , hfMatches :: [TcType] -- What the refinement variables got matched - -- with, if anything - , hfDoc :: Maybe HsDocString } -- Documentation of this HoleFit, if - -- available. - - -hfName :: HoleFit -> Name -hfName hf = case hfCand hf of - IdHFCand id -> idName id - NameHFCand name -> name - GreHFCand gre -> gre_name gre - -hfIsLcl :: HoleFit -> Bool -hfIsLcl hf = case hfCand hf of - IdHFCand _ -> True - NameHFCand _ -> False - GreHFCand gre -> gre_lcl gre - --- We define an Eq and Ord instance to be able to build a graph. -instance Eq HoleFit where - (==) = (==) `on` hfId - --- We compare HoleFits by their name instead of their Id, since we don't --- want our tests to be affected by the non-determinism of `nonDetCmpVar`, --- which is used to compare Ids. When comparing, we want HoleFits with a lower --- refinement level to come first. -instance Ord HoleFit where - compare a b = cmp a b - where cmp = if hfRefLvl a == hfRefLvl b - then compare `on` hfName - else compare `on` hfRefLvl - -instance Outputable HoleFit where - ppr = pprHoleFit debugHoleFitDispConfig - -- If enabled, we go through the fits and add any associated documentation, -- by looking it up in the module or the environment (for local fits) addDocs :: [HoleFit] -> TcM [HoleFit] @@ -499,70 +446,70 @@ addDocs fits = msg = text "TcHoleErrors addDocs" lookupInIface name (ModIface { mi_decl_docs = DeclDocMap dmap }) = Map.lookup name dmap - upd lclDocs fit = - let name = hfName fit in - do { doc <- if hfIsLcl fit - then pure (Map.lookup name lclDocs) - else do { mbIface <- loadInterfaceForNameMaybe msg name - ; return $ mbIface >>= lookupInIface name } - ; return $ fit {hfDoc = doc} } + upd lclDocs fit@(HoleFit {hfCand = cand}) = + do { let name = getName cand + ; doc <- if hfIsLcl fit + then pure (Map.lookup name lclDocs) + else do { mbIface <- loadInterfaceForNameMaybe msg name + ; return $ mbIface >>= lookupInIface name } + ; return $ fit {hfDoc = doc} } + upd _ fit = return fit -- For pretty printing hole fits, we display the name and type of the fit, -- with added '_' to represent any extra arguments in case of a non-zero -- refinement level. pprHoleFit :: HoleFitDispConfig -> HoleFit -> SDoc -pprHoleFit (HFDC sWrp sWrpVars sTy sProv sMs) hf = hang display 2 provenance - where name = hfName hf - ty = hfType hf - matches = hfMatches hf - wrap = hfWrap hf - tyApp = sep $ zipWithEqual "pprHoleFit" pprArg vars wrap - where pprArg b arg = case binderArgFlag b of - Specified -> text "@" <> pprParendType arg - -- Do not print type application for inferred - -- variables (#16456) - Inferred -> empty - Required -> pprPanic "pprHoleFit: bad Required" +pprHoleFit _ (RawHoleFit sd) = sd +pprHoleFit (HFDC sWrp sWrpVars sTy sProv sMs) (HoleFit {..}) = + hang display 2 provenance + where name = getName hfCand + tyApp = sep $ zipWithEqual "pprHoleFit" pprArg vars hfWrap + where pprArg b arg = case binderArgFlag b of + Specified -> text "@" <> pprParendType arg + -- Do not print type application for inferred + -- variables (#16456) + Inferred -> empty + Required -> pprPanic "pprHoleFit: bad Required" (ppr b <+> ppr arg) - tyAppVars = sep $ punctuate comma $ - zipWithEqual "pprHoleFit" (\v t -> ppr (binderVar v) <+> - text "~" <+> pprParendType t) - vars wrap - - vars = unwrapTypeVars ty - where - -- Attempts to get all the quantified type variables in a type, - -- e.g. - -- return :: forall (m :: * -> *) Monad m => (forall a . a -> m a) - -- into [m, a] - unwrapTypeVars :: Type -> [TyCoVarBinder] - unwrapTypeVars t = vars ++ case splitFunTy_maybe unforalled of - Just (_, unfunned) -> unwrapTypeVars unfunned - _ -> [] - where (vars, unforalled) = splitForAllVarBndrs t - holeVs = sep $ map (parens . (text "_" <+> dcolon <+>) . ppr) matches - holeDisp = if sMs then holeVs - else sep $ replicate (length matches) $ text "_" - occDisp = pprPrefixOcc name - tyDisp = ppWhen sTy $ dcolon <+> ppr ty - has = not . null - wrapDisp = ppWhen (has wrap && (sWrp || sWrpVars)) - $ text "with" <+> if sWrp || not sTy - then occDisp <+> tyApp - else tyAppVars - docs = case hfDoc hf of - Just d -> text "{-^" <> - (vcat . map text . lines . unpackHDS) d - <> text "-}" - _ -> empty - funcInfo = ppWhen (has matches && sTy) $ - text "where" <+> occDisp <+> tyDisp - subDisp = occDisp <+> if has matches then holeDisp else tyDisp - display = subDisp $$ nest 2 (funcInfo $+$ docs $+$ wrapDisp) - provenance = ppWhen sProv $ parens $ - case hfCand hf of - GreHFCand gre -> pprNameProvenance gre - _ -> text "bound at" <+> ppr (getSrcLoc name) + tyAppVars = sep $ punctuate comma $ + zipWithEqual "pprHoleFit" (\v t -> ppr (binderVar v) <+> + text "~" <+> pprParendType t) + vars hfWrap + + vars = unwrapTypeVars hfType + where + -- Attempts to get all the quantified type variables in a type, + -- e.g. + -- return :: forall (m :: * -> *) Monad m => (forall a . a -> m a) + -- into [m, a] + unwrapTypeVars :: Type -> [TyCoVarBinder] + unwrapTypeVars t = vars ++ case splitFunTy_maybe unforalled of + Just (_, unfunned) -> unwrapTypeVars unfunned + _ -> [] + where (vars, unforalled) = splitForAllVarBndrs t + holeVs = sep $ map (parens . (text "_" <+> dcolon <+>) . ppr) hfMatches + holeDisp = if sMs then holeVs + else sep $ replicate (length hfMatches) $ text "_" + occDisp = pprPrefixOcc name + tyDisp = ppWhen sTy $ dcolon <+> ppr hfType + has = not . null + wrapDisp = ppWhen (has hfWrap && (sWrp || sWrpVars)) + $ text "with" <+> if sWrp || not sTy + then occDisp <+> tyApp + else tyAppVars + docs = case hfDoc of + Just d -> text "{-^" <> + (vcat . map text . lines . unpackHDS) d + <> text "-}" + _ -> empty + funcInfo = ppWhen (has hfMatches && sTy) $ + text "where" <+> occDisp <+> tyDisp + subDisp = occDisp <+> if has hfMatches then holeDisp else tyDisp + display = subDisp $$ nest 2 (funcInfo $+$ docs $+$ wrapDisp) + provenance = ppWhen sProv $ parens $ + case hfCand of + GreHFCand gre -> pprNameProvenance gre + _ -> text "bound at" <+> ppr (getSrcLoc name) getLocalBindings :: TidyEnv -> Ct -> TcM [Id] getLocalBindings tidy_orig ct @@ -598,11 +545,15 @@ findValidHoleFits tidy_env implics simples ct | isExprHoleCt ct = ; maxVSubs <- maxValidHoleFits <$> getDynFlags ; hfdc <- getHoleFitDispConfig ; sortingAlg <- getSortingAlg + ; dflags <- getDynFlags + ; hfPlugs <- tcg_hf_plugins <$> getGblEnv ; let findVLimit = if sortingAlg > NoSorting then Nothing else maxVSubs - ; refLevel <- refLevelHoleFits <$> getDynFlags - ; traceTc "findingValidHoleFitsFor { " $ ppr ct + refLevel = refLevelHoleFits dflags + hole = TyH (listToBag relevantCts) implics (Just ct) + (candidatePlugins, fitPlugins) = + unzip $ map (\p-> ((candPlugin p) hole, (fitPlugin p) hole)) hfPlugs + ; traceTc "findingValidHoleFitsFor { " $ ppr hole ; traceTc "hole_lvl is:" $ ppr hole_lvl - ; traceTc "implics are: " $ ppr implics ; traceTc "simples are: " $ ppr simples ; traceTc "locals are: " $ ppr lclBinds ; let (lcl, gbl) = partition gre_lcl (globalRdrEnvElts rdr_env) @@ -615,11 +566,14 @@ findValidHoleFits tidy_env implics simples ct | isExprHoleCt ct = globals = map GreHFCand gbl syntax = map NameHFCand builtIns to_check = locals ++ syntax ++ globals + ; cands <- foldM (flip ($)) to_check candidatePlugins + ; traceTc "numPlugins are:" $ ppr (length candidatePlugins) ; (searchDiscards, subs) <- - tcFilterHoleFits findVLimit implics relevantCts (hole_ty, []) to_check + tcFilterHoleFits findVLimit hole (hole_ty, []) cands ; (tidy_env, tidy_subs) <- zonkSubs tidy_env subs ; tidy_sorted_subs <- sortFits sortingAlg tidy_subs - ; let (pVDisc, limited_subs) = possiblyDiscard maxVSubs tidy_sorted_subs + ; plugin_handled_subs <- foldM (flip ($)) tidy_sorted_subs fitPlugins + ; let (pVDisc, limited_subs) = possiblyDiscard maxVSubs plugin_handled_subs vDiscards = pVDisc || searchDiscards ; subs_with_docs <- addDocs limited_subs ; let vMsg = ppUnless (null subs_with_docs) $ @@ -638,8 +592,8 @@ findValidHoleFits tidy_env implics simples ct | isExprHoleCt ct = ; traceTc "ref_tys are" $ ppr ref_tys ; let findRLimit = if sortingAlg > NoSorting then Nothing else maxRSubs - ; refDs <- mapM (flip (tcFilterHoleFits findRLimit implics - relevantCts) to_check) ref_tys + ; refDs <- mapM (flip (tcFilterHoleFits findRLimit hole) + cands) ref_tys ; (tidy_env, tidy_rsubs) <- zonkSubs tidy_env $ concatMap snd refDs ; tidy_sorted_rsubs <- sortFits sortingAlg tidy_rsubs -- For refinement substitutions we want matches @@ -649,8 +603,10 @@ findValidHoleFits tidy_env implics simples ct | isExprHoleCt ct = ; (tidy_env, tidy_hole_ty) <- zonkTidyTcType tidy_env hole_ty ; let hasExactApp = any (tcEqType tidy_hole_ty) . hfWrap (exact, not_exact) = partition hasExactApp tidy_sorted_rsubs - (pRDisc, exact_last_rfits) = - possiblyDiscard maxRSubs $ not_exact ++ exact + ; plugin_handled_rsubs <- foldM (flip ($)) + (not_exact ++ exact) fitPlugins + ; let (pRDisc, exact_last_rfits) = + possiblyDiscard maxRSubs $ plugin_handled_rsubs rDiscards = pRDisc || any fst refDs ; rsubs_with_docs <- addDocs exact_last_rfits ; return (tidy_env, @@ -732,6 +688,9 @@ findValidHoleFits tidy_env implics simples ct | isExprHoleCt ct = where zonkSubs' zs env [] = return (env, reverse zs) zonkSubs' zs env (hf:hfs) = do { (env', z) <- zonkSub env hf ; zonkSubs' (z:zs) env' hfs } + + zonkSub :: TidyEnv -> HoleFit -> TcM (TidyEnv, HoleFit) + zonkSub env hf@RawHoleFit{} = return (env, hf) zonkSub env hf@HoleFit{hfType = ty, hfMatches = m, hfWrap = wrp} = do { (env, ty') <- zonkTidyTcType env ty ; (env, m') <- zonkTidyTcTypes env m @@ -786,10 +745,7 @@ findValidHoleFits env _ _ _ = return (env, empty) -- running the type checker. Stops after finding limit matches. tcFilterHoleFits :: Maybe Int -- ^ How many we should output, if limited - -> [Implication] - -- ^ Enclosing implications for givens - -> [Ct] - -- ^ Any relevant unsolved simple constraints + -> TypedHole -- ^ The hole to filter against -> (TcType, [TcTyVar]) -- ^ The type to check for fits and a list of refinement -- variables (free type variables in the type) for emulating @@ -799,8 +755,8 @@ tcFilterHoleFits :: Maybe Int -> TcM (Bool, [HoleFit]) -- ^ We return whether or not we stopped due to hitting the limit -- and the fits we found. -tcFilterHoleFits (Just 0) _ _ _ _ = return (False, []) -- Stop right away on 0 -tcFilterHoleFits limit implics relevantCts ht@(hole_ty, _) candidates = +tcFilterHoleFits (Just 0) _ _ _ = return (False, []) -- Stop right away on 0 +tcFilterHoleFits limit (TyH {..}) ht@(hole_ty, _) candidates = do { traceTc "checkingFitsFor {" $ ppr hole_ty ; (discards, subs) <- go [] emptyVarSet limit ht candidates ; traceTc "checkingFitsFor }" empty @@ -901,7 +857,7 @@ tcFilterHoleFits limit implics relevantCts ht@(hole_ty, _) candidates = -- refinement hole fits, so we can't wrap the side-effects deeper than this. withoutUnification fvs $ do { traceTc "checkingFitOf {" $ ppr ty - ; (fits, wrp) <- tcCheckHoleFit (listToBag relevantCts) implics h_ty ty + ; (fits, wrp) <- tcCheckHoleFit hole h_ty ty ; traceTc "Did it fit?" $ ppr fits ; traceTc "wrap is: " $ ppr wrp ; traceTc "checkingFitOf }" empty @@ -934,6 +890,7 @@ tcFilterHoleFits limit implics relevantCts ht@(hole_ty, _) candidates = else return Nothing } else return Nothing } where fvs = mkFVs ref_vars `unionFV` hole_fvs `unionFV` tyCoFVsOfType ty + hole = TyH tyHRelevantCts tyHImplics Nothing subsDiscardMsg :: SDoc @@ -970,8 +927,8 @@ withoutUnification free_vars action = -- discarding any errors. Subsumption here means that the ty_b can fit into the -- ty_a, i.e. `tcSubsumes a b == True` if b is a subtype of a. tcSubsumes :: TcSigmaType -> TcSigmaType -> TcM Bool -tcSubsumes ty_a ty_b = fst <$> tcCheckHoleFit emptyBag [] ty_a ty_b - +tcSubsumes ty_a ty_b = fst <$> tcCheckHoleFit dummyHole ty_a ty_b + where dummyHole = TyH emptyBag [] Nothing -- | A tcSubsumes which takes into account relevant constraints, to fix trac -- #14273. This makes sure that when checking whether a type fits the hole, @@ -979,24 +936,22 @@ tcSubsumes ty_a ty_b = fst <$> tcCheckHoleFit emptyBag [] ty_a ty_b -- constraints on the type of the hole. -- Note: The simplifier may perform unification, so make sure to restore any -- free type variables to avoid side-effects. -tcCheckHoleFit :: Cts -- ^ Any relevant Cts to the hole. - -> [Implication] - -- ^ The nested implications of the hole with the innermost - -- implication first. - -> TcSigmaType -- ^ The type of the hole. - -> TcSigmaType -- ^ The type to check whether fits. +tcCheckHoleFit :: TypedHole -- ^ The hole to check against + -> TcSigmaType + -- ^ The type to check against (possibly modified, e.g. refined) + -> TcSigmaType -- ^ The type to check whether fits. -> TcM (Bool, HsWrapper) -- ^ Whether it was a match, and the wrapper from hole_ty to ty. -tcCheckHoleFit _ _ hole_ty ty | hole_ty `eqType` ty +tcCheckHoleFit _ hole_ty ty | hole_ty `eqType` ty = return (True, idHsWrapper) -tcCheckHoleFit relevantCts implics hole_ty ty = discardErrs $ +tcCheckHoleFit (TyH {..}) hole_ty ty = discardErrs $ do { -- We wrap the subtype constraint in the implications to pass along the -- givens, and so we must ensure that any nested implications and skolems -- end up with the correct level. The implications are ordered so that -- the innermost (the one with the highest level) is first, so it -- suffices to get the level of the first one (or the current level, if -- there are no implications involved). - innermost_lvl <- case implics of + innermost_lvl <- case tyHImplics of [] -> getTcLevel -- imp is the innermost implication (imp:_) -> return (ic_tclvl imp) @@ -1004,15 +959,15 @@ tcCheckHoleFit relevantCts implics hole_ty ty = discardErrs $ tcSubType_NC ExprSigCtxt ty hole_ty ; traceTc "Checking hole fit {" empty ; traceTc "wanteds are: " $ ppr wanted - ; if isEmptyWC wanted && isEmptyBag relevantCts + ; if isEmptyWC wanted && isEmptyBag tyHRelevantCts then traceTc "}" empty >> return (True, wrp) else do { fresh_binds <- newTcEvBinds -- The relevant constraints may contain HoleDests, so we must -- take care to clone them as well (to avoid #15370). - ; cloned_relevants <- mapBagM cloneWanted relevantCts + ; cloned_relevants <- mapBagM cloneWanted tyHRelevantCts -- We wrap the WC in the nested implications, see -- Note [Nested Implications] - ; let outermost_first = reverse implics + ; let outermost_first = reverse tyHImplics setWC = setWCAndBinds fresh_binds -- We add the cloned relevants to the wanteds generated by -- the call to tcSubType_NC, see Note [Relevant Constraints] @@ -1035,3 +990,10 @@ tcCheckHoleFit relevantCts implics hole_ty ty = discardErrs $ setWCAndBinds binds imp wc = WC { wc_simple = emptyBag , wc_impl = unitBag $ imp { ic_wanted = wc , ic_binds = binds } } + +-- | Maps a plugin that needs no state to one with an empty one. +fromPureHFPlugin :: HoleFitPlugin -> HoleFitPluginR +fromPureHFPlugin plug = + HoleFitPluginR { hfPluginInit = newTcRef () + , hfPluginRun = const plug + , hfPluginStop = const $ return () } diff --git a/compiler/typecheck/TcHoleFitTypes.hs b/compiler/typecheck/TcHoleFitTypes.hs new file mode 100644 index 0000000000..8700cc1399 --- /dev/null +++ b/compiler/typecheck/TcHoleFitTypes.hs @@ -0,0 +1,144 @@ +{-# LANGUAGE ExistentialQuantification #-} +module TcHoleFitTypes ( + TypedHole (..), HoleFit (..), HoleFitCandidate (..), + CandPlugin, FitPlugin, HoleFitPlugin (..), HoleFitPluginR (..), + hfIsLcl, pprHoleFitCand + ) where + +import GhcPrelude + +import TcRnTypes +import TcType + +import RdrName + +import HsDoc +import Id + +import Outputable +import Name + +import Data.Function ( on ) + +data TypedHole = TyH { tyHRelevantCts :: Cts + -- ^ Any relevant Cts to the hole + , tyHImplics :: [Implication] + -- ^ The nested implications of the hole with the + -- innermost implication first. + , tyHCt :: Maybe Ct + -- ^ The hole constraint itself, if available. + } + +instance Outputable TypedHole where + ppr (TyH rels implics ct) + = hang (text "TypedHole") 2 + (ppr rels $+$ ppr implics $+$ ppr ct) + + +-- | HoleFitCandidates are passed to hole fit plugins and then +-- checked whether they fit a given typed-hole. +data HoleFitCandidate = IdHFCand Id -- An id, like locals. + | NameHFCand Name -- A name, like built-in syntax. + | GreHFCand GlobalRdrElt -- A global, like imported ids. + deriving (Eq) + +instance Outputable HoleFitCandidate where + ppr = pprHoleFitCand + +pprHoleFitCand :: HoleFitCandidate -> SDoc +pprHoleFitCand (IdHFCand cid) = text "Id HFC: " <> ppr cid +pprHoleFitCand (NameHFCand cname) = text "Name HFC: " <> ppr cname +pprHoleFitCand (GreHFCand cgre) = text "Gre HFC: " <> ppr cgre + + + + +instance NamedThing HoleFitCandidate where + getName hfc = case hfc of + IdHFCand cid -> idName cid + NameHFCand cname -> cname + GreHFCand cgre -> gre_name cgre + getOccName hfc = case hfc of + IdHFCand cid -> occName cid + NameHFCand cname -> occName cname + GreHFCand cgre -> occName (gre_name cgre) + +instance HasOccName HoleFitCandidate where + occName = getOccName + +instance Ord HoleFitCandidate where + compare = compare `on` getName + +-- | HoleFit is the type we use for valid hole fits. It contains the +-- element that was checked, the Id of that element as found by `tcLookup`, +-- and the refinement level of the fit, which is the number of extra argument +-- holes that this fit uses (e.g. if hfRefLvl is 2, the fit is for `Id _ _`). +data HoleFit = + HoleFit { hfId :: Id -- ^ The elements id in the TcM + , hfCand :: HoleFitCandidate -- ^ The candidate that was checked. + , hfType :: TcType -- ^ The type of the id, possibly zonked. + , hfRefLvl :: Int -- ^ The number of holes in this fit. + , hfWrap :: [TcType] -- ^ The wrapper for the match. + , hfMatches :: [TcType] + -- ^ What the refinement variables got matched with, if anything + , hfDoc :: Maybe HsDocString + -- ^ Documentation of this HoleFit, if available. + } + | RawHoleFit SDoc + -- ^ A fit that is just displayed as is. Here so thatHoleFitPlugins + -- can inject any fit they want. + +-- We define an Eq and Ord instance to be able to build a graph. +instance Eq HoleFit where + (==) = (==) `on` hfId + +instance Outputable HoleFit where + ppr (RawHoleFit sd) = sd + ppr (HoleFit _ cand ty _ _ mtchs _) = + hang (name <+> holes) 2 (text "where" <+> name <+> dcolon <+> (ppr ty)) + where name = ppr $ getName cand + holes = sep $ map (parens . (text "_" <+> dcolon <+>) . ppr) mtchs + +-- We compare HoleFits by their name instead of their Id, since we don't +-- want our tests to be affected by the non-determinism of `nonDetCmpVar`, +-- which is used to compare Ids. When comparing, we want HoleFits with a lower +-- refinement level to come first. +instance Ord HoleFit where + compare (RawHoleFit _) (RawHoleFit _) = EQ + compare (RawHoleFit _) _ = LT + compare _ (RawHoleFit _) = GT + compare a@(HoleFit {}) b@(HoleFit {}) = cmp a b + where cmp = if hfRefLvl a == hfRefLvl b + then compare `on` (getName . hfCand) + else compare `on` hfRefLvl + +hfIsLcl :: HoleFit -> Bool +hfIsLcl hf@(HoleFit {}) = case hfCand hf of + IdHFCand _ -> True + NameHFCand _ -> False + GreHFCand gre -> gre_lcl gre +hfIsLcl _ = False + + +-- | A plugin for modifying the candidate hole fits *before* they're checked. +type CandPlugin = TypedHole -> [HoleFitCandidate] -> TcM [HoleFitCandidate] + +-- | A plugin for modifying hole fits *after* they've been found. +type FitPlugin = TypedHole -> [HoleFit] -> TcM [HoleFit] + +-- | A HoleFitPlugin is a pair of candidate and fit plugins. +data HoleFitPlugin = HoleFitPlugin + { candPlugin :: CandPlugin + , fitPlugin :: FitPlugin } + +-- | HoleFitPluginR adds a TcRef to hole fit plugins so that plugins can +-- track internal state. Note the existential quantification, ensuring that +-- the state cannot be modified from outside the plugin. +data HoleFitPluginR = forall s. HoleFitPluginR + { hfPluginInit :: TcM (TcRef s) + -- ^ Initializes the TcRef to be passed to the plugin + , hfPluginRun :: TcRef s -> HoleFitPlugin + -- ^ The function defining the plugin itself + , hfPluginStop :: TcRef s -> TcM () + -- ^ Cleanup of state, guaranteed to be called even on error + } diff --git a/compiler/typecheck/TcHoleFitTypes.hs-boot b/compiler/typecheck/TcHoleFitTypes.hs-boot new file mode 100644 index 0000000000..fde064e51a --- /dev/null +++ b/compiler/typecheck/TcHoleFitTypes.hs-boot @@ -0,0 +1,10 @@ +-- This boot file is in place to break the loop where: +-- + TcRnTypes needs 'HoleFitPlugin', +-- + which needs 'TcHoleFitTypes' +-- + which needs 'TcRnTypes' +module TcHoleFitTypes where + +-- Build ordering +import GHC.Base() + +data HoleFitPlugin diff --git a/compiler/typecheck/TcRnDriver.hs b/compiler/typecheck/TcRnDriver.hs index 3ffc5df61e..55c229766f 100644 --- a/compiler/typecheck/TcRnDriver.hs +++ b/compiler/typecheck/TcRnDriver.hs @@ -140,6 +140,9 @@ import qualified Data.Set as S import Control.DeepSeq import Control.Monad +import TcHoleFitTypes ( HoleFitPluginR (..) ) + + #include "HsVersions.h" {- @@ -164,7 +167,7 @@ tcRnModule hsc_env mod_sum save_rn_syntax (text "Renamer/typechecker"<+>brackets (ppr this_mod)) (const ()) $ initTc hsc_env hsc_src save_rn_syntax this_mod real_loc $ - withTcPlugins hsc_env $ + withTcPlugins hsc_env $ withHoleFitPlugins hsc_env $ tcRnModuleTcRnM hsc_env mod_sum parsedModule pair @@ -1840,7 +1843,7 @@ runTcInteractive :: HscEnv -> TcRn a -> IO (Messages, Maybe a) -- Initialise the tcg_inst_env with instances from all home modules. -- This mimics the more selective call to hptInstances in tcRnImports runTcInteractive hsc_env thing_inside - = initTcInteractive hsc_env $ withTcPlugins hsc_env $ + = initTcInteractive hsc_env $ withTcPlugins hsc_env $ withHoleFitPlugins hsc_env $ do { traceTc "setInteractiveContext" $ vcat [ text "ic_tythings:" <+> vcat (map ppr (ic_tythings icxt)) , text "ic_insts:" <+> vcat (map (pprBndr LetBind . instanceDFunId) ic_insts) @@ -2880,6 +2883,30 @@ withTcPlugins hsc_env m = getTcPlugins :: DynFlags -> [TcRnMonad.TcPlugin] getTcPlugins dflags = catMaybes $ mapPlugins dflags (\p args -> tcPlugin p args) + +withHoleFitPlugins :: HscEnv -> TcM a -> TcM a +withHoleFitPlugins hsc_env m = + case (getHfPlugins (hsc_dflags hsc_env)) of + [] -> m -- Common fast case + plugins -> do (plugins,stops) <- unzip `fmap` mapM startPlugin plugins + -- This ensures that hfPluginStop is called even if a type + -- error occurs during compilation. + eitherRes <- tryM $ do + updGblEnv (\e -> e { tcg_hf_plugins = plugins }) m + sequence_ stops + case eitherRes of + Left _ -> failM + Right res -> return res + where + startPlugin (HoleFitPluginR init plugin stop) = + do ref <- init + return (plugin ref, stop ref) + +getHfPlugins :: DynFlags -> [HoleFitPluginR] +getHfPlugins dflags = + catMaybes $ mapPlugins dflags (\p args -> holeFitPlugin p args) + + runRenamerPlugin :: TcGblEnv -> HsGroup GhcRn -> TcM (TcGblEnv, HsGroup GhcRn) diff --git a/compiler/typecheck/TcRnMonad.hs b/compiler/typecheck/TcRnMonad.hs index 9a76e9ced8..e297301b6b 100644 --- a/compiler/typecheck/TcRnMonad.hs +++ b/compiler/typecheck/TcRnMonad.hs @@ -312,6 +312,7 @@ initTc hsc_env hsc_src keep_rn_syntax mod loc do_this tcg_safeInfer = infer_var, tcg_dependent_files = dependent_files_var, tcg_tc_plugins = [], + tcg_hf_plugins = [], tcg_top_loc = loc, tcg_static_wc = static_wc_var, tcg_complete_matches = [], diff --git a/compiler/typecheck/TcRnTypes.hs b/compiler/typecheck/TcRnTypes.hs index 3bd26e9f76..c8d83215fd 100644 --- a/compiler/typecheck/TcRnTypes.hs +++ b/compiler/typecheck/TcRnTypes.hs @@ -208,6 +208,8 @@ import Data.Maybe ( mapMaybe ) import GHCi.Message import GHCi.RemoteTypes +import {-# SOURCE #-} TcHoleFitTypes ( HoleFitPlugin ) + import qualified Language.Haskell.TH as TH -- | A 'NameShape' is a substitution on 'Name's that can be used @@ -685,6 +687,8 @@ data TcGblEnv tcg_tc_plugins :: [TcPluginSolver], -- ^ A list of user-defined plugins for the constraint solver. + tcg_hf_plugins :: [HoleFitPlugin], + -- ^ A list of user-defined plugins for hole fit suggestions. tcg_top_loc :: RealSrcSpan, -- ^ The RealSrcSpan this module came from diff --git a/docs/users_guide/extending_ghc.rst b/docs/users_guide/extending_ghc.rst index ee816bf1b0..6b6a1ed3cb 100644 --- a/docs/users_guide/extending_ghc.rst +++ b/docs/users_guide/extending_ghc.rst @@ -834,6 +834,329 @@ output: typeCheckPlugin (tc): {$trModule = Module (TrNameS "main"#) (TrNameS "A"#), a = ()} +.. _hole-fit-plugins + +Hole fit plugins +~~~~~~~~~~~~~~~~ + +Hole-fit plugins are plugins that are called when a typed-hole error message is +being generated, and allows you to access information about the typed-hole at +compile time, and allows you to customize valid hole fit suggestions. + +Using hole-fit plugins, you can extend the behavior of valid hole fit +suggestions to use e.g. Hoogle or other external tools to find and/or synthesize +valid hole fits, with the same information about the typed-hole that GHC uses. + +There are two access points are bundled together for defining hole fit plugins, +namely a candidate plugin and a fit plugin, for modifying the candidates to be +checked and fits respectively. + + +:: + + type CandPlugin = TypedHole -> [HoleFitCandidate] -> TcM [HoleFitCandidate] + + type FitPlugin = TypedHole -> [HoleFit] -> TcM [HoleFit] + + data HoleFitPlugin = HoleFitPlugin + { candPlugin :: CandPlugin + -- ^ A plugin for modifying hole fit candidates before they're checked + , fitPlugin :: FitPlugin + -- ^ A plugin for modifying valid hole fits after they've been found. + } + +Where ``TypedHole`` contains all the information about the hole available to GHC +at error generation. + +:: + + data TypedHole = TyH { tyHRelevantCts :: Cts + -- ^ Any relevant Cts to the hole + , tyHImplics :: [Implication] + -- ^ The nested implications of the hole with the + -- innermost implication first. + , tyHCt :: Maybe Ct + -- ^ The hole constraint itself, if available. + } + +``HoleFitPlugins`` are then defined as follows + +:: + + plugin :: Plugin + plugin = defaultPlugin { + holeFitPlugin = (fmap . fmap) fromPureHFPlugin hfPlugin + } + + + hfPlugin :: [CommandLineOption] -> Maybe HoleFitPlugin + + +Where ``fromPureHFPlugin :: HoleFitPlugin -> HoleFitPluginR`` is a convencience +function provided in the ``TcHoleErrors`` module, for defining plugins that do +not require internal state. + + +Stateful hole fit plugins +^^^^^^^^^^^^^^^^^^^^^^^^^ + + +``HoleFitPlugins`` are wrapped in a ``HoleFitPluginR``, which provides a +``TcRef`` for the plugin to use to track internal state, and to facilitate +communication between the candidate and fit plugin. + +:: + + -- | HoleFitPluginR adds a TcRef to hole fit plugins so that plugins can + -- track internal state. Note the existential quantification, ensuring that + -- the state cannot be modified from outside the plugin. + data HoleFitPluginR = forall s. HoleFitPluginR + { hfPluginInit :: TcM (TcRef s) + -- ^ Initializes the TcRef to be passed to the plugin + , hfPluginRun :: TcRef s -> HoleFitPlugin + -- ^ The function defining the plugin itself + , hfPluginStop :: TcRef s -> TcM () + -- ^ Cleanup of state, guaranteed to be called even on error + } + +The plugin is then defined as by providing a value for the ``holeFitPlugin`` +field, a function that takes the ``CommandLineOption`` strings that are passed +to the compiler using the :ghc-flag:`-fplugin-opt` flags and returns a +``HoleFitPluginR``. This function can be used to pass the ``CommandLineOption`` +strings along to the candidate and fit plugins respectively. + + + +Hole fit plugin example +^^^^^^^^^^^^^^^^^^^^^^^ + +The following plugins allows users to limit the search for valid hole fits to +certain modules, to sort the hole fits by where they originated (in ascending or +descending order), as well as allowing users to put a limit on how much time is +spent on searching for valid hole fits, after which new searches are aborted. + +:: + + {-# LANGUAGE TypeApplications, RecordWildCards #-} + module HolePlugin where + + import GhcPlugins hiding ((<>)) + + import TcHoleErrors + + import Data.List (stripPrefix, sortOn) + + import TcRnTypes + + import TcRnMonad + + import Data.Time (UTCTime, NominalDiffTime) + import qualified Data.Time as Time + + import Text.Read + + + data HolePluginState = HPS { timeAlloted :: Maybe NominalDiffTime + , elapsedTime :: NominalDiffTime + , timeCurStarted :: UTCTime } + + bumpElapsed :: NominalDiffTime -> HolePluginState -> HolePluginState + bumpElapsed ad (HPS a e t) = HPS a (e + ad) t + + setAlloted :: Maybe NominalDiffTime -> HolePluginState -> HolePluginState + setAlloted a (HPS _ e t) = HPS a e t + + setCurStarted :: UTCTime -> HolePluginState -> HolePluginState + setCurStarted nt (HPS a e _) = HPS a e nt + + hpStartState :: HolePluginState + hpStartState = HPS Nothing zero undefined + where zero = fromInteger @NominalDiffTime 0 + + initPlugin :: [CommandLineOption] -> TcM (TcRef HolePluginState) + initPlugin [msecs] = newTcRef $ hpStartState { timeAlloted = alloted } + where + errMsg = "Invalid amount of milliseconds given to plugin: " <> show msecs + alloted = case readMaybe @Integer msecs of + Just millisecs -> Just $ fromInteger @NominalDiffTime millisecs / 1000 + _ -> error errMsg + initPlugin _ = newTcRef hpStartState + + fromModule :: HoleFitCandidate -> [String] + fromModule (GreHFCand gre) = + map (moduleNameString . importSpecModule) $ gre_imp gre + fromModule _ = [] + + toHoleFitCommand :: TypedHole -> String -> Maybe String + toHoleFitCommand TyH{tyHCt = Just (CHoleCan _ h)} str + = stripPrefix ("_" <> str) $ occNameString $ holeOcc h + toHoleFitCommand _ _ = Nothing + + -- | This candidate plugin filters the candidates by module, + -- using the name of the hole as module to search in + modFilterTimeoutP :: [CommandLineOption] -> TcRef HolePluginState -> CandPlugin + modFilterTimeoutP _ ref hole cands = do + curTime <- liftIO Time.getCurrentTime + HPS {..} <- readTcRef ref + updTcRef ref (setCurStarted curTime) + return $ case timeAlloted of + -- If we're out of time we remove all the candidates. Then nothing is checked. + Just sofar | elapsedTime > sofar -> [] + _ -> case toHoleFitCommand hole "only_" of + + Just modName -> filter (inScopeVia modName) cands + _ -> cands + where inScopeVia modNameStr cand@(GreHFCand _) = + elem (toModName modNameStr) $ fromModule cand + inScopeVia _ _ = False + toModName = replace '_' '.' + replace :: Eq a => a -> a -> [a] -> [a] + replace _ _ [] = [] + replace a b (x:xs) = (if x == a then b else x):replace a b xs + + modSortP :: [CommandLineOption] -> TcRef HolePluginState -> FitPlugin + modSortP _ ref hole hfs = do + curTime <- liftIO Time.getCurrentTime + HPS {..} <- readTcRef ref + updTcRef ref $ bumpElapsed (Time.diffUTCTime curTime timeCurStarted) + return $ case timeAlloted of + -- If we're out of time, remove any candidates, so nothing is checked. + Just sofar | elapsedTime > sofar -> [RawHoleFit $ text msg] + _ -> case toHoleFitCommand hole "sort_by_mod" of + -- If only_ is on, the fits will all be from the same module. + Just ('_':'d':'e':'s':'c':_) -> reverse hfs + Just _ -> orderByModule hfs + _ -> hfs + where orderByModule :: [HoleFit] -> [HoleFit] + orderByModule = sortOn (fmap fromModule . mbHFCand) + mbHFCand :: HoleFit -> Maybe HoleFitCandidate + mbHFCand HoleFit {hfCand = c} = Just c + mbHFCand _ = Nothing + msg = hang (text "Error: The time ran out, and the search was aborted for this hole.") + 7 $ text "Try again with a longer timeout." + + plugin :: Plugin + plugin = defaultPlugin { holeFitPlugin = holeFitP, pluginRecompile = purePlugin} + + holeFitP :: [CommandLineOption] -> Maybe HoleFitPluginR + holeFitP opts = Just (HoleFitPluginR initP pluginDef stopP) + where initP = initPlugin opts + stopP = const $ return () + pluginDef ref = HoleFitPlugin { candPlugin = modFilterTimeoutP opts ref + , fitPlugin = modSortP opts ref } + +When you then compile a module containing the following + +:: + + {-# OPTIONS -fplugin=HolePlugin + -fplugin-opt=HolePlugin:600 + -funclutter-valid-hole-fits #-} + module Main where + + import Prelude hiding (head, last) + + import Data.List (head, last) + + + f, g, h, i, j :: [Int] -> Int + f = _too_long + j = _ + i = _sort_by_mod_desc + g = _only_Data_List + h = _only_Prelude + + main :: IO () + main = return () + + +The output is as follows: + +.. code-block:: none + + Main.hs:12:5: error: + • Found hole: _too_long :: [Int] -> Int + Or perhaps ‘_too_long’ is mis-spelled, or not in scope + • In the expression: _too_long + In an equation for ‘f’: f = _too_long + • Relevant bindings include + f :: [Int] -> Int (bound at Main.hs:12:1) + Valid hole fits include + Error: The time ran out, and the search was aborted for this hole. + Try again with a longer timeout. + | + 12 | f = _too_long + | ^^^^^^^^^ + + Main.hs:13:5: error: + • Found hole: _ :: [Int] -> Int + • In the expression: _ + In an equation for ‘j’: j = _ + • Relevant bindings include + j :: [Int] -> Int (bound at Main.hs:13:1) + Valid hole fits include + j :: [Int] -> Int + f :: [Int] -> Int + g :: [Int] -> Int + h :: [Int] -> Int + i :: [Int] -> Int + head :: forall a. [a] -> a + (Some hole fits suppressed; use -fmax-valid-hole-fits=N or -fno-max-valid-hole-fits) + | + 13 | j = _ + | ^ + + Main.hs:14:5: error: + • Found hole: _sort_by_mod_desc :: [Int] -> Int + Or perhaps ‘_sort_by_mod_desc’ is mis-spelled, or not in scope + • In the expression: _sort_by_mod_desc + In an equation for ‘i’: i = _sort_by_mod_desc + • Relevant bindings include + i :: [Int] -> Int (bound at Main.hs:14:1) + Valid hole fits include + sum :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + product :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + minimum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + maximum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + length :: forall (t :: * -> *) a. Foldable t => t a -> Int + last :: forall a. [a] -> a + (Some hole fits suppressed; use -fmax-valid-hole-fits=N or -fno-max-valid-hole-fits) + | + 14 | i = _sort_by_mod_desc + | ^^^^^^^^^^^^^^^^^ + + Main.hs:15:5: error: + • Found hole: _only_Data_List :: [Int] -> Int + Or perhaps ‘_only_Data_List’ is mis-spelled, or not in scope + • In the expression: _only_Data_List + In an equation for ‘g’: g = _only_Data_List + • Relevant bindings include + g :: [Int] -> Int (bound at Main.hs:15:1) + Valid hole fits include + head :: forall a. [a] -> a + last :: forall a. [a] -> a + | + 15 | g = _only_Data_List + | ^^^^^^^^^^^^^^^ + + Main.hs:16:5: error: + • Found hole: _only_Prelude :: [Int] -> Int + Or perhaps ‘_only_Prelude’ is mis-spelled, or not in scope + • In the expression: _only_Prelude + In an equation for ‘h’: h = _only_Prelude + • Relevant bindings include + h :: [Int] -> Int (bound at Main.hs:16:1) + Valid hole fits include + length :: forall (t :: * -> *) a. Foldable t => t a -> Int + maximum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + minimum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + product :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + sum :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + | + 16 | h = _only_Prelude + | ^^^^^^^^^^^^^ + + .. _plugin_recompilation: diff --git a/testsuite/tests/plugins/Makefile b/testsuite/tests/plugins/Makefile index f58a771ef9..46ef8cb3eb 100644 --- a/testsuite/tests/plugins/Makefile +++ b/testsuite/tests/plugins/Makefile @@ -130,3 +130,7 @@ T16104: T16260: "$(TEST_HC)" $(TEST_HC_OPTS) $(ghcPluginWayFlags) -v0 T16260.hs -package-db simple-plugin/pkg.T16260/local.package.conf -fplugin Simple.TrustworthyPlugin "$(TEST_HC)" $(TEST_HC_OPTS) $(ghcPluginWayFlags) -v0 T16260.hs -package-db simple-plugin/pkg.T16260/local.package.conf -fplugin Simple.TrustworthyPlugin -fplugin-trustworthy + +.PHONY: HoleFitPlugin +HoleFitPlugin: + "$(TEST_HC)" $(TEST_HC_OPTS) $(ghcPluginWayFlags) -v0 HoleFitPlugin.hs -package-db hole-fit-plugin/pkg.hole-fit-plugin/local.package.conf diff --git a/testsuite/tests/plugins/all.T b/testsuite/tests/plugins/all.T index aea3748767..a4273f5ae8 100644 --- a/testsuite/tests/plugins/all.T +++ b/testsuite/tests/plugins/all.T @@ -200,8 +200,16 @@ test('T16104', ], makefile_test, []) + test('T16260', [extra_files(['simple-plugin/']), pre_cmd('$MAKE -s --no-print-directory -C simple-plugin package.T16260 TOP={top}') ], makefile_test, []) + +test('test-hole-plugin', + [extra_files(['hole-fit-plugin/']), + pre_cmd('$MAKE -s --no-print-directory -C hole-fit-plugin package.hole-fit-plugin TOP={top}'), + extra_hc_opts('-package-db hole-fit-plugin/pkg.hole-fit-plugin/local.package.conf '+ config.plugin_way_flags) + ], + compile, ['-fdefer-typed-holes']) diff --git a/testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.cabal b/testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.cabal new file mode 100644 index 0000000000..344fccf461 --- /dev/null +++ b/testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.cabal @@ -0,0 +1,11 @@ +name: HoleFitPlugin +cabal-version: >= 1.24 +build-type: Simple +version: 1.0.0 + + +library + default-language: Haskell2010 + build-depends: base, ghc, time + exposed-modules: HoleFitPlugin + ghc-options: -Wall diff --git a/testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.hs b/testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.hs new file mode 100644 index 0000000000..dc6e9762f5 --- /dev/null +++ b/testsuite/tests/plugins/hole-fit-plugin/HoleFitPlugin.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE TypeApplications, RecordWildCards #-} +module HoleFitPlugin where + +import GhcPlugins hiding ((<>)) + +import TcHoleErrors + +import Data.List (stripPrefix, sortOn) + +import TcRnTypes + +import TcRnMonad + +import Text.Read + + + +data HolePluginState = HPS { holesChecked :: Int + , holesLimit :: Maybe Int} + +bumpHolesChecked :: HolePluginState -> HolePluginState +bumpHolesChecked (HPS h l) = HPS (h + 1) l + +initPlugin :: [CommandLineOption] -> TcM (TcRef HolePluginState) +initPlugin [limit] = newTcRef $ HPS 0 $ + case readMaybe @Int limit of + Just number -> Just number + _ -> error $ "Invalid argument to plugin: " <> show limit +initPlugin _ = newTcRef $ HPS 0 Nothing + +fromModule :: HoleFitCandidate -> [String] +fromModule (GreHFCand gre) = + map (moduleNameString . importSpecModule) $ gre_imp gre +fromModule _ = [] + +toHoleFitCommand :: TypedHole -> String -> Maybe String +toHoleFitCommand TyH{tyHCt = Just (CHoleCan _ h)} str + = stripPrefix ("_" <> str) $ occNameString $ holeOcc h +toHoleFitCommand _ _ = Nothing + + +-- | This candidate plugin filters the candidates by module, +-- using the name of the hole as module to search in +modFilterTimeoutP :: [CommandLineOption] -> TcRef HolePluginState -> CandPlugin +modFilterTimeoutP _ ref hole cands = do + updTcRef ref bumpHolesChecked + HPS {..} <- readTcRef ref + return $ case holesLimit of + -- If we're out of checks, remove any candidates, so nothing is checked. + Just limit | holesChecked > limit -> [] + _ -> case toHoleFitCommand hole "only_" of + Just modName -> filter (inScopeVia modName) cands + _ -> cands + where inScopeVia modNameStr cand@(GreHFCand _) = + elem (toModName modNameStr) $ fromModule cand + inScopeVia _ _ = False + toModName = replace '_' '.' + replace :: Eq a => a -> a -> [a] -> [a] + replace _ _ [] = [] + replace a b (x:xs) = (if x == a then b else x):replace a b xs + + +modSortP :: [CommandLineOption] -> TcRef HolePluginState -> FitPlugin +modSortP _ ref hole hfs = do + HPS {..} <- readTcRef ref + return $ case holesLimit of + Just limit | holesChecked > limit -> [RawHoleFit $ text msg] + _ -> case toHoleFitCommand hole "sort_by_mod" of + -- If only_ is on, the fits will all be from the same module. + Just ('_':'d':'e':'s':'c':_) -> reverse hfs + Just _ -> orderByModule hfs + _ -> hfs + where orderByModule :: [HoleFit] -> [HoleFit] + orderByModule = sortOn (fmap fromModule . mbHFCand) + mbHFCand :: HoleFit -> Maybe HoleFitCandidate + mbHFCand HoleFit {hfCand = c} = Just c + mbHFCand _ = Nothing + msg = "Error: Too many holes were checked, and the search aborted for" + <> "this hole. Try again with a higher limit." + +plugin :: Plugin +plugin = defaultPlugin { holeFitPlugin = holeFitP, pluginRecompile = purePlugin} + +holeFitP :: [CommandLineOption] -> Maybe HoleFitPluginR +holeFitP opts = Just (HoleFitPluginR initP pluginDef stopP) + where initP = initPlugin opts + stopP = const $ return () + pluginDef ref = HoleFitPlugin { candPlugin = modFilterTimeoutP opts ref + , fitPlugin = modSortP opts ref } diff --git a/testsuite/tests/plugins/hole-fit-plugin/Makefile b/testsuite/tests/plugins/hole-fit-plugin/Makefile new file mode 100644 index 0000000000..7ce5b78e75 --- /dev/null +++ b/testsuite/tests/plugins/hole-fit-plugin/Makefile @@ -0,0 +1,18 @@ +TOP=../../.. +include $(TOP)/mk/boilerplate.mk +include $(TOP)/mk/test.mk + +clean.%: + rm -rf pkg.$* + +HERE := $(abspath .) +$(eval $(call canonicalise,HERE)) + +package.%: + $(MAKE) -s --no-print-directory clean.$* + mkdir pkg.$* + "$(TEST_HC)" -outputdir pkg.$* --make -v0 -o pkg.$*/setup Setup.hs + "$(GHC_PKG)" init pkg.$*/local.package.conf + pkg.$*/setup configure --distdir pkg.$*/dist -v0 $(CABAL_PLUGIN_BUILD) --prefix="$(HERE)/pkg.$*/install" --with-compiler="$(TEST_HC)" $(if $(findstring YES,$(HAVE_PROFILING)), --enable-library-profiling) --with-hc-pkg="$(GHC_PKG)" --package-db=pkg.$*/local.package.conf + pkg.$*/setup build --distdir pkg.$*/dist -v0 + pkg.$*/setup install --distdir pkg.$*/dist -v0 diff --git a/testsuite/tests/plugins/hole-fit-plugin/Setup.hs b/testsuite/tests/plugins/hole-fit-plugin/Setup.hs new file mode 100644 index 0000000000..e8ef27dbba --- /dev/null +++ b/testsuite/tests/plugins/hole-fit-plugin/Setup.hs @@ -0,0 +1,3 @@ +import Distribution.Simple + +main = defaultMain diff --git a/testsuite/tests/plugins/test-hole-plugin.hs b/testsuite/tests/plugins/test-hole-plugin.hs new file mode 100644 index 0000000000..dc6b67e1d2 --- /dev/null +++ b/testsuite/tests/plugins/test-hole-plugin.hs @@ -0,0 +1,19 @@ +{-# OPTIONS -fplugin=HoleFitPlugin + -fplugin-opt=HoleFitPlugin:4 + -funclutter-valid-hole-fits #-} +module Main where + +import Prelude hiding (head, last) + +import Data.List (head, last) + + +f, g, h, i, j :: [Int] -> Int +f = _too_long +j = _ +i = _sort_by_mod_desc +g = _only_Data_List +h = _only_Prelude + +main :: IO () +main = return () diff --git a/testsuite/tests/plugins/test-hole-plugin.stderr b/testsuite/tests/plugins/test-hole-plugin.stderr new file mode 100644 index 0000000000..7ca539e8d7 --- /dev/null +++ b/testsuite/tests/plugins/test-hole-plugin.stderr @@ -0,0 +1,66 @@ + +test-hole-plugin.hs:12:5: warning: [-Wtyped-holes (in -Wdefault)] + • Found hole: _too_long :: [Int] -> Int + Or perhaps ‘_too_long’ is mis-spelled, or not in scope + • In the expression: _too_long + In an equation for ‘f’: f = _too_long + • Relevant bindings include + f :: [Int] -> Int (bound at test-hole-plugin.hs:12:1) + Valid hole fits include + Error: Too many holes were checked, and the search aborted forthis hole. Try again with a higher limit. + +test-hole-plugin.hs:13:5: warning: [-Wtyped-holes (in -Wdefault)] + • Found hole: _ :: [Int] -> Int + • In the expression: _ + In an equation for ‘j’: j = _ + • Relevant bindings include + j :: [Int] -> Int (bound at test-hole-plugin.hs:13:1) + Valid hole fits include + j :: [Int] -> Int + f :: [Int] -> Int + i :: [Int] -> Int + g :: [Int] -> Int + h :: [Int] -> Int + head :: forall a. [a] -> a + (Some hole fits suppressed; use -fmax-valid-hole-fits=N or -fno-max-valid-hole-fits) + +test-hole-plugin.hs:14:5: warning: [-Wtyped-holes (in -Wdefault)] + • Found hole: _sort_by_mod_desc :: [Int] -> Int + Or perhaps ‘_sort_by_mod_desc’ is mis-spelled, or not in scope + • In the expression: _sort_by_mod_desc + In an equation for ‘i’: i = _sort_by_mod_desc + • Relevant bindings include + i :: [Int] -> Int (bound at test-hole-plugin.hs:14:1) + Valid hole fits include + sum :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + product :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + minimum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + maximum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + length :: forall (t :: * -> *) a. Foldable t => t a -> Int + last :: forall a. [a] -> a + (Some hole fits suppressed; use -fmax-valid-hole-fits=N or -fno-max-valid-hole-fits) + +test-hole-plugin.hs:15:5: warning: [-Wtyped-holes (in -Wdefault)] + • Found hole: _only_Data_List :: [Int] -> Int + Or perhaps ‘_only_Data_List’ is mis-spelled, or not in scope + • In the expression: _only_Data_List + In an equation for ‘g’: g = _only_Data_List + • Relevant bindings include + g :: [Int] -> Int (bound at test-hole-plugin.hs:15:1) + Valid hole fits include + head :: forall a. [a] -> a + last :: forall a. [a] -> a + +test-hole-plugin.hs:16:5: warning: [-Wtyped-holes (in -Wdefault)] + • Found hole: _only_Prelude :: [Int] -> Int + Or perhaps ‘_only_Prelude’ is mis-spelled, or not in scope + • In the expression: _only_Prelude + In an equation for ‘h’: h = _only_Prelude + • Relevant bindings include + h :: [Int] -> Int (bound at test-hole-plugin.hs:16:1) + Valid hole fits include + length :: forall (t :: * -> *) a. Foldable t => t a -> Int + maximum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + minimum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a + product :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a + sum :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a |