Skip to content

Wingman: Destruct on empty case #1721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6733246
Add typechecking machinery
isovector Apr 12, 2021
9f91fb4
Add smallestQ SYB traversal
isovector Apr 12, 2021
f688888
emptyCaseQ to find an empty case
isovector Apr 12, 2021
6bbd7c1
Get scrutinee and its type if hovering over an empty case split
isovector Apr 12, 2021
a0a669e
Allow ghcide to operate on emptycase
isovector Apr 12, 2021
f2378f4
[wip] not working, but most of the pieces are here
isovector Apr 12, 2021
8dcea25
Revert "[wip] not working, but most of the pieces are here"
isovector Apr 14, 2021
8230d0c
Got it hooked up as a code lens, modulo actually putting in the ctors
isovector Apr 14, 2021
67ff25f
It works as a code lens!
isovector Apr 14, 2021
f69d068
Allow MagicHash; gracefully exit if cant typecheck
isovector Apr 15, 2021
6d1882b
Don't shadow bindings
isovector Apr 15, 2021
23e5d06
Extract the src span of the case's match group
isovector Apr 15, 2021
848f9d2
Improve how parenthesizing happens in ExactPrint
isovector Apr 15, 2021
eba9631
Remove a field that pedantic is yelling about
isovector Apr 15, 2021
af77d0a
It works!
isovector Apr 15, 2021
4d8b476
Tidying and renaming
isovector Apr 15, 2021
0ad5186
Move EmptyCase into its own module
isovector Apr 15, 2021
a99bad3
Don't need the SYB changes after all
isovector Apr 15, 2021
0e8c577
Haddock
isovector Apr 15, 2021
8e6d3a6
Don't need this function either
isovector Apr 15, 2021
b1fc26f
Revert "Extract the src span of the case's match group"
isovector Apr 15, 2021
57dc63e
Use noExtField for compatability
isovector Apr 15, 2021
efdd338
Use PatCompat for destructionFor
isovector Apr 15, 2021
8cb7efb
Guard behind a feature
isovector Apr 15, 2021
9805ee8
Forgot to move a few more things into EmptyCase
isovector Apr 15, 2021
941f9c2
Remove the orphan instance
isovector Apr 15, 2021
58d4017
Add EmptyCase tests
isovector Apr 15, 2021
a5be902
Seriously fuck hlint
isovector Apr 15, 2021
e1cb51e
GADT apartness test
isovector Apr 15, 2021
54abd75
Fix splice test
isovector Apr 15, 2021
471d22d
Don't forget about branding!
isovector Apr 15, 2021
462986a
Merge branch 'master' into empty-case
mergify[bot] Apr 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ghcide/session-loader/Development/IDE/Session.hs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import HieDb.Create
import HieDb.Types
import HieDb.Utils
import Maybes (MaybeT (runMaybeT))
import GHC.LanguageExtensions (Extension(EmptyCase))

-- | Bump this version number when making changes to the format of the data stored in hiedb
hiedbDataVersion :: String
Expand Down Expand Up @@ -771,6 +772,7 @@ setOptions (ComponentOptions theOpts compRoot _) dflags = do
setIgnoreInterfacePragmas $
setLinkerOptions $
disableOptimisation $
allowEmptyCaseButWithWarning $
setUpTypedHoles $
makeDynFlagsAbsolute compRoot dflags'
-- initPackages parses the -package flags and
Expand All @@ -780,6 +782,14 @@ setOptions (ComponentOptions theOpts compRoot _) dflags = do
return (final_df, targets)


-- | Wingman wants to support destructing of empty cases, but these are a parse
-- error by default. So we want to enable 'EmptyCase', but then that leads to
-- silent errors without 'Opt_WarnIncompletePatterns'.
allowEmptyCaseButWithWarning :: DynFlags -> DynFlags
allowEmptyCaseButWithWarning =
flip xopt_set EmptyCase . flip wopt_set Opt_WarnIncompletePatterns


-- we don't want to generate object code so we compile to bytecode
-- (HscInterpreted) which implies LinkInMemory
-- HscInterpreted
Expand Down
2 changes: 1 addition & 1 deletion ghcide/src/Development/IDE/Core/UseStale.hs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ newtype Tracked (age :: Age) a = UnsafeTracked
-- change. Use the 'Category' instance to compose 'PositionMapping's in order
-- to transform between values of different stale ages.
newtype PositionMap (from :: Age) (to :: Age) = PositionMap
{ getPositionMapping :: P.PositionMapping
{ _getPositionMapping :: P.PositionMapping
}

instance Category PositionMap where
Expand Down
66 changes: 52 additions & 14 deletions ghcide/src/Development/IDE/GHC/ExactPrint.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Development.IDE.GHC.ExactPrint
annotateDecl,
hoistGraft,
graftWithM,
graftExprWithM,
genericGraftWithSmallestM,
genericGraftWithLargestM,
graftSmallestDeclsWithM,
Expand Down Expand Up @@ -66,11 +67,9 @@ import Parser (parseIdentifier)
import Data.Traversable (for)
import Data.Foldable (Foldable(fold))
import Data.Bool (bool)
import Data.Monoid (All(All), Any(Any))
import Data.Monoid (All(All), Any(Any), getAll)
import Data.Functor.Compose (Compose(Compose))
#if __GLASGOW_HASKELL__ == 808
import Control.Arrow
#endif


------------------------------------------------------------------------------
Expand Down Expand Up @@ -246,25 +245,64 @@ graftExpr ::
LHsExpr GhcPs ->
Graft (Either String) a
graftExpr dst val = Graft $ \dflags a -> do
-- Traverse the tree, looking for our replacement node. But keep track of
-- the context (parent HsExpr constructor) we're in while we do it. This
-- lets us determine wehther or not we need parentheses.
let (All needs_parens, All needs_space) =
everythingWithContext (All True, All True) (<>)
( mkQ (mempty, ) $ \x s -> case x of
(L src _ :: LHsExpr GhcPs) | src == dst ->
(s, s)
L _ x' -> (mempty, needsParensSpace x')
) a
let (needs_space, mk_parens) = getNeedsSpaceAndParenthesize dst a

runGraft
(graft' needs_space dst $ bool id maybeParensAST needs_parens val)
(graft' needs_space dst $ mk_parens val)
dflags
a


getNeedsSpaceAndParenthesize ::
(ASTElement ast, Data a) =>
SrcSpan ->
a ->
(Bool, Located ast -> Located ast)
getNeedsSpaceAndParenthesize dst a =
-- Traverse the tree, looking for our replacement node. But keep track of
-- the context (parent HsExpr constructor) we're in while we do it. This
-- lets us determine wehther or not we need parentheses.
let (needs_parens, needs_space) =
everythingWithContext (Nothing, Nothing) (<>)
( mkQ (mempty, ) $ \x s -> case x of
(L src _ :: LHsExpr GhcPs) | src == dst ->
(s, s)
L _ x' -> (mempty, Just *** Just $ needsParensSpace x')
) a
in ( maybe True getAll needs_space
, bool id maybeParensAST $ maybe False getAll needs_parens
)


------------------------------------------------------------------------------

graftExprWithM ::
forall m a.
(Fail.MonadFail m, Data a) =>
SrcSpan ->
(LHsExpr GhcPs -> TransformT m (Maybe (LHsExpr GhcPs))) ->
Graft m a
graftExprWithM dst trans = Graft $ \dflags a -> do
let (needs_space, mk_parens) = getNeedsSpaceAndParenthesize dst a

everywhereM'
( mkM $
\case
val@(L src _ :: LHsExpr GhcPs)
| src == dst -> do
mval <- trans val
case mval of
Just val' -> do
(anns, val'') <-
hoistTransform (either Fail.fail pure) $
annotate dflags needs_space $ mk_parens val'
modifyAnnsT $ mappend anns
pure val''
Nothing -> pure val
l -> pure l
)
a

graftWithM ::
forall ast m a.
(Fail.MonadFail m, Data a, ASTElement ast) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ module TTypeAppExp where
import Data.Proxy

f :: Proxy Int
f = (Proxy @Int)
f = Proxy @Int
2 changes: 2 additions & 0 deletions plugins/hls-tactics-plugin/hls-tactics-plugin.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ library
Wingman.CodeGen.Utils
Wingman.Context
Wingman.Debug
Wingman.EmptyCase
Wingman.FeatureSet
Wingman.GHC
Wingman.Judgements
Expand Down Expand Up @@ -121,6 +122,7 @@ test-suite tests
CodeAction.IntrosSpec
CodeAction.RefineSpec
CodeAction.UseDataConSpec
CodeLens.EmptyCaseSpec
ProviderSpec
Spec
UnificationSpec
Expand Down
36 changes: 31 additions & 5 deletions plugins/hls-tactics-plugin/src/Wingman/CodeGen.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}

module Wingman.CodeGen
( module Wingman.CodeGen
Expand Down Expand Up @@ -47,6 +48,8 @@ destructMatches
-- ^ Type being destructed
-> Judgement
-> RuleM (Synthesized [RawMatch])
-- TODO(sandy): In an ideal world, this would be the same codepath as
-- 'destructionFor'. Make sure to change that if you ever change this.
destructMatches f scrut t jdg = do
let hy = jEntireHypothesis jdg
g = jGoal jdg
Expand All @@ -65,7 +68,7 @@ destructMatches f scrut t jdg = do
args = conLikeInstOrigArgTys' con apps
modify $ appEndo $ foldMap (Endo . evidenceToSubst) ev
subst <- gets ts_unifier
names <- mkManyGoodNames (hyNamesInScope hy) args
let names = mkManyGoodNames (hyNamesInScope hy) args
let hy' = patternHypothesis scrut con jdg
$ zip names
$ coerce args
Expand All @@ -81,6 +84,29 @@ destructMatches f scrut t jdg = do
& #syn_val %~ match [mkDestructPat con names] . unLoc


------------------------------------------------------------------------------
-- | Generate just the 'Match'es for a case split on a specific type.
destructionFor :: Hypothesis a -> Type -> Maybe [LMatch GhcPs (LHsExpr GhcPs)]
-- TODO(sandy): In an ideal world, this would be the same codepath as
-- 'destructMatches'. Make sure to change that if you ever change this.
destructionFor hy t = do
case tacticsGetDataCons t of
Nothing -> Nothing
Just ([], _) -> Nothing
Just (dcs, apps) -> do
for dcs $ \dc -> do
let con = RealDataCon dc
args = conLikeInstOrigArgTys' con apps
names = mkManyGoodNames (hyNamesInScope hy) args
pure
. noLoc
. Match noExtField CaseAlt [toPatCompat $ mkDestructPat con names]
. GRHSs noExtField (pure $ noLoc $ GRHS noExtField [] $ noLoc $ var "_")
. noLoc
$ EmptyLocalBinds noExtField



------------------------------------------------------------------------------
-- | Produces a pattern for a data con and the names of its fields.
mkDestructPat :: ConLike -> [OccName] -> Pat GhcPs
Expand Down
4 changes: 2 additions & 2 deletions plugins/hls-tactics-plugin/src/Wingman/CodeGen/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Data.List
import DataCon
import Development.IDE.GHC.Compat
import GHC.Exts
import GHC.SourceGen (RdrNameStr, recordConE, string)
import GHC.SourceGen (RdrNameStr (UnqualStr), recordConE, string)
import GHC.SourceGen.Overloaded
import GhcPlugins (nilDataCon, charTy, eqType)
import Name
Expand Down Expand Up @@ -43,7 +43,7 @@ mkCon con apps (fmap unLoc -> args)


coerceName :: HasOccName a => a -> RdrNameStr
coerceName = fromString . occNameString . occName
coerceName = UnqualStr . fromString . occNameString . occName


------------------------------------------------------------------------------
Expand Down
157 changes: 157 additions & 0 deletions plugins/hls-tactics-plugin/src/Wingman/EmptyCase.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
{-# LANGUAGE OverloadedStrings #-}

module Wingman.EmptyCase where

import Control.Applicative (empty)
import Control.Monad
import Control.Monad.Except (runExcept)
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
import Data.Aeson
import Data.Generics.Aliases (mkQ, GenericQ)
import Data.Generics.Schemes (everything)
import Data.Maybe
import Data.Monoid
import qualified Data.Text as T
import Data.Traversable
import Development.IDE (hscEnv)
import Development.IDE (realSrcSpanToRange)
import Development.IDE.Core.RuleTypes
import Development.IDE.Core.Shake (IdeState (..))
import Development.IDE.Core.UseStale
import Development.IDE.GHC.Compat
import Development.IDE.GHC.ExactPrint
import Development.IDE.Spans.LocalBindings (getLocalScope)
import Ide.Types
import Language.LSP.Server
import Language.LSP.Types
import OccName
import Prelude hiding (span)
import Prelude hiding (span)
import TcRnTypes (tcg_binds)
import Wingman.CodeGen (destructionFor)
import Wingman.FeatureSet
import Wingman.GHC
import Wingman.Judgements
import Wingman.LanguageServer
import Wingman.Types


------------------------------------------------------------------------------
-- | The 'CommandId' for the empty case completion.
emptyCaseLensCommandId :: CommandId
emptyCaseLensCommandId = CommandId "wingman.emptyCase"


------------------------------------------------------------------------------
-- | A command function that just applies a 'WorkspaceEdit'.
workspaceEditHandler :: CommandFunction IdeState WorkspaceEdit
workspaceEditHandler _ideState wedit = do
_ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ())
return $ Right Null


------------------------------------------------------------------------------
-- | Provide the "empty case completion" code lens
codeLensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens
codeLensProvider state plId (CodeLensParams _ _ (TextDocumentIdentifier uri))
| Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do
cfg <- getTacticConfig plId
ccs <- getClientCapabilities
liftIO $ fromMaybeT (Right $ List []) $ do
guard $ hasFeature FeatureEmptyCase $ cfg_feature_set cfg

dflags <- getIdeDynflags state nfp
TrackedStale pm _ <- runStaleIde state nfp GetAnnotatedParsedSource
TrackedStale binds bind_map <- runStaleIde state nfp GetBindings
holes <- emptyCaseScrutinees state nfp

fmap (Right . List) $ for holes $ \(ss, ty) -> do
binds_ss <- liftMaybe $ mapAgeFrom bind_map ss
let bindings = getLocalScope (unTrack binds) $ unTrack binds_ss
range = realSrcSpanToRange $ unTrack ss
matches <-
liftMaybe $
destructionFor
(foldMap (hySingleton . occName . fst) bindings)
ty
edits <- liftMaybe $ hush $
mkWorkspaceEdits dflags ccs uri (unTrack pm) $
graftMatchGroup (RealSrcSpan $ unTrack ss) $
noLoc matches

pure $
CodeLens range
(Just
$ mkLspCommand
plId
emptyCaseLensCommandId
(mkEmptyCaseLensDesc ty)
$ Just $ pure $ toJSON $ edits
)
Nothing
codeLensProvider _ _ _ = pure $ Right $ List []


------------------------------------------------------------------------------
-- | The description for the empty case lens.
mkEmptyCaseLensDesc :: Type -> T.Text
mkEmptyCaseLensDesc ty =
"Wingman: Complete case constructors (" <> T.pack (unsafeRender ty) <> ")"


------------------------------------------------------------------------------
-- | Silence an error.
hush :: Either e a -> Maybe a
hush (Left _) = Nothing
hush (Right a) = Just a


------------------------------------------------------------------------------
-- | Graft a 'RunTacticResults' into the correct place in an AST. Correctly
-- deals with top-level holes, in which we might need to fiddle with the
-- 'Match's that bind variables.
graftMatchGroup
:: SrcSpan
-> Located [LMatch GhcPs (LHsExpr GhcPs)]
-> Graft (Either String) ParsedSource
graftMatchGroup ss l =
hoistGraft (runExcept . runExceptString) $ graftExprWithM ss $ \case
L span (HsCase ext scrut mg@_) -> do
pure $ Just $ L span $ HsCase ext scrut $ mg { mg_alts = l }
(_ :: LHsExpr GhcPs) -> pure Nothing


fromMaybeT :: Functor m => a -> MaybeT m a -> m a
fromMaybeT def = fmap (fromMaybe def) . runMaybeT


------------------------------------------------------------------------------
-- | Find the last typechecked module, and find the most specific span, as well
-- as the judgement at the given range.
emptyCaseScrutinees
:: IdeState
-> NormalizedFilePath
-> MaybeT IO [(Tracked 'Current RealSrcSpan, Type)]
emptyCaseScrutinees state nfp = do
TrackedStale tcg tcg_map <- fmap (fmap tmrTypechecked) $ runStaleIde state nfp TypeCheck
let tcg' = unTrack tcg
hscenv <- runStaleIde state nfp GhcSessionDeps

let scrutinees = traverse (emptyCaseQ . tcg_binds) tcg
for scrutinees $ \aged@(unTrack -> (ss, scrutinee)) -> do
ty <- MaybeT $ typeCheck (hscEnv $ untrackedStaleValue hscenv) tcg' scrutinee
case ss of
RealSrcSpan r -> do
rss' <- liftMaybe $ mapAgeTo tcg_map $ unsafeCopyAge aged r
pure (rss', ty)
UnhelpfulSpan _ -> empty


------------------------------------------------------------------------------
-- | Get the 'SrcSpan' and scrutinee of every empty case.
emptyCaseQ :: GenericQ [(SrcSpan, HsExpr GhcTc)]
emptyCaseQ = everything (<>) $ mkQ mempty $ \case
L new_span (Case scrutinee []) -> pure (new_span, scrutinee)
(_ :: LHsExpr GhcTc) -> mempty

Loading