large-records-0.2.0.0: src/Data/Record/Plugin.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ViewPatterns #-}
-- | Support for scalable large records
--
-- = Usage
--
-- > {-# OPTIONS_GHC -fplugin=Data.Record.Plugin #-}
-- >
-- > {-# ANN type B LargeRecordStrict #-}
-- > data B a = B {a :: a, b :: String}
-- > deriving stock (Show, Eq, Ord)
--
-- See 'LargeRecordOptions' for the list of all possible annotations.
--
-- = Usage with @record-dot-preprocessor@
--
-- There are two important points. First, the order of plugins matters —
-- @record-dot-preprocessor@ has to be listed before this plugin (and
-- correspondingly will be applied /after/ this plugin):
--
-- > {-# OPTIONS_GHC -fplugin=RecordDotPreprocessor -fplugin=Data.Record.Plugin #-}
--
-- Second, you will want at least version 0.2.14.
module Data.Record.Plugin (plugin) where
import Control.Monad.Except
import Control.Monad.Trans.Writer.CPS
import Data.List (intersperse)
import Data.Map.Strict (Map)
import Data.Set (Set)
import Data.Traversable (for)
import Language.Haskell.TH (Extension(..))
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import Data.Record.Internal.Plugin.CodeGen (genLargeRecord)
import Data.Record.Internal.GHC.Fresh
import Data.Record.Internal.GHC.Shim
import Data.Record.Internal.GHC.TemplateHaskellStyle
import Data.Record.Internal.Plugin.Exception
import Data.Record.Internal.Plugin.Options
import Data.Record.Internal.Plugin.Record
{-------------------------------------------------------------------------------
Top-level: the plugin proper
-------------------------------------------------------------------------------}
plugin :: Plugin
plugin = defaultPlugin {
parsedResultAction = aux
, pluginRecompile = purePlugin
}
where
aux ::
[CommandLineOption]
-> ModSummary
-> HsParsedModule -> Hsc HsParsedModule
aux _opts _summary parsed@HsParsedModule{hpm_module = modl} = do
modl' <- transformDecls modl
pure $ parsed { hpm_module = modl' }
{-------------------------------------------------------------------------------
Transform datatype declarations
-------------------------------------------------------------------------------}
transformDecls :: LHsModule -> Hsc LHsModule
transformDecls (L l modl@HsModule {hsmodDecls = decls, hsmodImports}) = do
(decls', transformed) <- runWriterT $ for decls $ transformDecl largeRecords
checkEnabledExtensions l
-- Check for annotations without corresponding types
let untransformed = Map.keysSet largeRecords `Set.difference` transformed
unless (Set.null untransformed) $ do
issueError l $ vcat $
text "These large-record annotations were not applied:"
: [text (" - " ++ n) | n <- Set.toList untransformed]
-- We add imports whether or not there were some errors, to avoid spurious
-- additional errors from ghc about things not in scope.
pure $ L l $ modl {
hsmodDecls = concat decls'
, hsmodImports = hsmodImports ++ map (uncurry importDecl) requiredImports
}
where
largeRecords :: Map String [(SrcSpan, LargeRecordOptions)]
largeRecords = getLargeRecordOptions modl
-- Required imports along with whether or not they should be qualified
--
-- ANN pragmas are written by the user, and should thefore not require
-- qualification; references to the runtime are generated by the plugin.
requiredImports :: [(ModuleName, Bool)]
requiredImports = [
(mkModuleName "Data.Record.Plugin.Options", False)
, (mkModuleName "Data.Record.Plugin.Runtime", True)
, (mkModuleName "GHC.Generics", True)
]
transformDecl ::
Map String [(SrcSpan, LargeRecordOptions)]
-> LHsDecl GhcPs
-> WriterT (Set String) Hsc [LHsDecl GhcPs]
transformDecl largeRecords decl@(L l _) =
case decl of
DataD (nameBase -> name) _ _ _ ->
case Map.findWithDefault [] name largeRecords of
[] ->
-- Not a large record. Leave alone.
return [decl]
(_:_:_) -> do
lift $ issueError l $ text ("Conflicting annotations for " ++ name)
return [decl]
[(annLoc, opts)] -> do
tell (Set.singleton name)
case runExcept (viewRecord annLoc opts decl) of
Left e -> do
lift $ issueError (exceptionLoc e) (exceptionToSDoc e)
-- Return the declaration unchanged if we cannot parse it
return [decl]
Right r -> do
newDecls <- lift $ runFreshHsc $ genLargeRecord r
when (debugLargeRecords opts) $
lift $ issueWarning l (debugMsg newDecls)
pure newDecls
_otherwise ->
pure [decl]
where
debugMsg :: [LHsDecl GhcPs] -> SDoc
debugMsg newDecls = pprSetDepth AllTheWay $ vcat $
text "large-records: splicing in the following definitions:"
: map ppr newDecls
{-------------------------------------------------------------------------------
Check for enabled extensions
In ghc 8.10 and up there are DynFlags plugins, which we could use to enable
these extensions for the user. Since this is not available in 8.8 however we
will not make use of this for now. (There is also reason to believe that these
may be removed again in later ghc releases.)
-------------------------------------------------------------------------------}
checkEnabledExtensions :: SrcSpan -> Hsc ()
checkEnabledExtensions l = do
dynFlags <- getDynFlags
let missing :: [RequiredExtension]
missing = filter (not . isEnabled dynFlags) requiredExtensions
unless (null missing) $
-- We issue a warning here instead of an error, for better integration
-- with HLS. Frankly, I'm not entirely sure what's going on there.
issueWarning l $ vcat . concat $ [
[text "Please enable these extensions for use with large-records:"]
, map ppr missing
]
where
requiredExtensions :: [RequiredExtension]
requiredExtensions = [
RequiredExtension [ConstraintKinds]
, RequiredExtension [DataKinds]
, RequiredExtension [ExistentialQuantification, GADTs]
, RequiredExtension [FlexibleInstances]
, RequiredExtension [MultiParamTypeClasses]
, RequiredExtension [ScopedTypeVariables]
, RequiredExtension [TypeFamilies]
, RequiredExtension [UndecidableInstances]
]
-- | Required extension
--
-- The list is used to represent alternative extensions that could all work
-- (e.g., @GADTs@ and @ExistentialQuantification@).
data RequiredExtension = RequiredExtension [Extension]
instance Outputable RequiredExtension where
ppr (RequiredExtension exts) = hsep . intersperse (text "or") $ map ppr exts
isEnabled :: DynFlags -> RequiredExtension -> Bool
isEnabled dynflags (RequiredExtension exts) = any (`xopt` dynflags) exts
{-------------------------------------------------------------------------------
Internal auxiliary
-------------------------------------------------------------------------------}
issueError :: SrcSpan -> SDoc -> Hsc ()
issueError l errMsg = do
dynFlags <- getDynFlags
throwOneError $
mkErrMsg dynFlags l neverQualify errMsg
issueWarning :: SrcSpan -> SDoc -> Hsc ()
issueWarning l errMsg = do
dynFlags <- getDynFlags
liftIO $ printOrThrowWarnings dynFlags . listToBag . (:[]) $
mkWarnMsg dynFlags l neverQualify errMsg