packages feed

core-warn-0.1.0.0: src/CoreWarn.hs

{-# LANGUAGE CPP #-}

#if __GLASGOW_HASKELL__ >= 810
#define REASON NoReason
#else
#define REASON
#endif

#if __GLASGOW_HASKELL__ >= 900
#define VARBINDARG
#else
#define VARBINDARG _
#endif

module CoreWarn (plugin) where

import Warn.Coercion
import Warn.Dictionary
import Control.Monad
import Data.Bool (bool)
import Data.Foldable
import Data.Graph.Good
import Data.IORef
import Data.Maybe (fromMaybe)
import Data.Monoid
import GHC (GhcTc)
import Generics.SYB
import Prelude hiding (lookup)
import System.IO.Unsafe (unsafePerformIO)
import qualified Data.Map as M
import qualified Data.Set as S

#if __GLASGOW_HASKELL__ >= 900
import GHC.Core.Stats
import GHC.Plugins hiding (typeSize, (<>))
import GHC.Tc.Types  (tcg_binds)
import Data.List (nub)
#else
import Data.Containers.ListUtils (nubOrd)
import CoreStats
import GhcPlugins hiding (typeSize, (<>))
import TcRnMonad (tcg_binds)
#endif

#if __GLASGOW_HASKELL__ >= 810
import GHC.Hs.Expr
import GHC.Hs.Binds
#else
import HsExpr
import HsBinds
#endif

------------------------------------------------------------------------------
-- | The main @core-warn@ program.
plugin :: Plugin
plugin =
  defaultPlugin
    { installCoreToDos = \ss ctds -> do
        binds <- liftIO $ readIORef global_tcg_ref
        pure $ ctds ++ [coreWarn (parseOpts ss) binds]
    , pluginRecompile = const $ pure NoForceRecompile
    , typeCheckResultAction = \_ _ tcg -> do
        liftIO $ writeIORef global_tcg_ref $ tcg_binds tcg
        pure tcg
    }


------------------------------------------------------------------------------
-- | We need to get our grubby little hands on the 'tcg_binds', but the
-- core-to-core plugin interface doesn't give us access to them. So we do this
-- very safe trick to get a hold of them.
global_tcg_ref :: IORef (LHsBinds GhcTc)
global_tcg_ref = unsafePerformIO $ newIORef $ error "no tcg_binds set"
{-# NOINLINE global_tcg_ref #-}


------------------------------------------------------------------------------
-- | Options for @core-warn@. These are opt-out.
data CoreWarnOpts = CoreWarnOpts
  { cwo_warnBigCoerces :: Endo Bool,
    cwo_warnDeepDicts :: Endo Bool
  }

instance Semigroup CoreWarnOpts where
  (<>) (CoreWarnOpts lb4 lb5) (CoreWarnOpts lb lb3) =
    CoreWarnOpts
      { cwo_warnBigCoerces = lb <> lb4,
        cwo_warnDeepDicts = lb3 <> lb5
      }

instance Monoid CoreWarnOpts where
  mempty =
    CoreWarnOpts
      { cwo_warnBigCoerces = mempty,
        cwo_warnDeepDicts = mempty
      }


------------------------------------------------------------------------------
-- | Parse options.
parseOpts :: [CommandLineOption] -> CoreWarnOpts
parseOpts = go
  where
    go = foldMap $ \case
      "warn-large-coercions"    -> CoreWarnOpts (Endo $ pure True) mempty
      "no-warn-large-coercions" -> CoreWarnOpts (Endo $ pure False) mempty
      "warn-deep-dicts"         -> CoreWarnOpts mempty (Endo $ pure True)
      "no-warn-deep-dicts"      -> CoreWarnOpts mempty (Endo $ pure False)
      _ -> mempty


------------------------------------------------------------------------------
-- | Given an 'OccName' corresponding to a dictionary, find every immediate
-- 'SrcSpan's that contain it.
findDictRef :: Data a => OccName -> a -> [SrcSpan]
findDictRef occ = everything (<>) $ mkQ mempty $ \case
#if __GLASGOW_HASKELL__ >= 900
  L loc (XExpr (WrapExpr ev))
#else
  L loc (HsWrap _ ev _)
#endif
    | isGoodSrcSpan loc ->
        everything (<>)
          (mkQ mempty $ \(v :: Var) -> bool [] [loc] $ getOccName v == occ)
          ev
  (_ :: LHsExpr GhcTc) -> []


------------------------------------------------------------------------------
-- | Given an 'OccName', find the src span for every coercion inside of its
-- definition.
findBindCoercions :: Data a => Name -> a -> [SrcSpan]
findBindCoercions occ = everything (<>) $ mkQ mempty $ \case
  x@(VarBind _ a _ VARBINDARG)
    | getName a == occ ->
        get_sub x
  x@(FunBind _ (L _ a) _ _ VARBINDARG)
    | getName a == occ ->
        get_sub x
  x@(AbsBinds _ _ b e _ _ _)
    | any ((== occ) . getName) b
   || any ((== occ) . getName . abe_poly) e -> get_sub x
  (_ :: HsBindLR GhcTc GhcTc) -> []
  where
    get_sub x =
      everything (<>) (mkQ mempty $ \case
#if __GLASGOW_HASKELL__ >= 900
        L loc (XExpr (WrapExpr y))
#else
        L loc (HsWrap _ y _)
#endif
          | isGoodSrcSpan loc
          , gtypecount (undefined :: Coercion) y > 0  -> [loc]
        (_ :: LHsExpr GhcTc) -> []
                      ) x


------------------------------------------------------------------------------
-- | Like 'fromMaybe' but for lists.
singletonIfEmpty :: a -> [a] -> [a]
singletonIfEmpty a as = if null as then [a] else as


------------------------------------------------------------------------------
-- | Is this 'CoreBndr' the 'Var' of a dictionary?
isDictVar :: CoreBndr -> Bool
isDictVar bndr = fromMaybe False $ do
  (tycon, _) <- tcSplitTyConApp_maybe $ idType bndr
  _cls <- tyConClass_maybe tycon
  pure True


------------------------------------------------------------------------------
-- | Translatea @'Bind' 'CoreBndr'@ into a map from 'CoreBndr's to 'CoreExpr's.
coreBndrToExprMap :: Bind CoreBndr -> M.Map CoreBndr CoreExpr
coreBndrToExprMap (NonRec var ex) = M.singleton var ex
coreBndrToExprMap (Rec ex) = foldMap (uncurry M.singleton) ex


------------------------------------------------------------------------------
-- | The @core-warn@ todo pass.
coreWarn :: CoreWarnOpts -> LHsBinds GhcTc -> CoreToDo
coreWarn opts binds = CoreDoPluginPass "coercionCheck" $ \guts -> do
  let programMap = foldMap coreBndrToExprMap $ mg_binds guts
      dictSets = fmap (S.fromList . toList)
                 . components
                 . graphFromEdges
                 . filter (isDictVar . fst)
                 . M.toList . fmap S.toList
                 . mkCoreAdjacencyMap
                 $ programMap

  when (flip appEndo True $ cwo_warnDeepDicts opts) $
    for_ dictSets \dictSet -> do
      let srcSpans
            = filter isGoodSrcSpan
            $ foldMap (flip findDictRef binds)
            $ foldMap (S.singleton . occName) dictSet
      when (shouldWarnDeepDict dictSet) $
        warnMsg REASON $
          pprDeepDict srcSpans dictSet

  when (flip appEndo True $ cwo_warnBigCoerces opts) $
    for_ (M.toList . fmap exprStats $ programMap) \(coreBndr, coreStats) ->
      when (shouldWarnLargeCoercion coreStats) $
        warnMsg REASON $
          pprWarnLargeCoerce
            (singletonIfEmpty noSrcSpan $
#if __GLASGOW_HASKELL__ >= 900
              -- TODO(sandy): I know it's slow, but blame GHC9 for getting rid
              -- of the 'Ord' instance on 'SrcSpan
              nub
#else
              nubOrd
#endif
              $ findBindCoercions (getName coreBndr) binds)
            coreBndr
            coreStats
  pure guts