early-0.0.0: src/EarlyPlugin.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
module EarlyPlugin (plugin) where
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.State.Strict
import qualified Data.Generics as SYB
import Data.Text (Text)
import qualified Data.Text as T
import qualified "ghc" GhcPlugins as GHC
import "ghc" HsExtension (GhcPs)
import "ghc" HsSyn
import "ghc" OccName
import "ghc" SrcLoc
import Text.Read
plugin :: GHC.Plugin
plugin = GHC.defaultPlugin
{ GHC.parsedResultAction = \cliOptions -> pluginImpl cliOptions
, GHC.pluginRecompile = GHC.purePlugin
}
pluginImpl :: [GHC.CommandLineOption] -> GHC.ModSummary -> GHC.HsParsedModule -> GHC.Hsc GHC.HsParsedModule
pluginImpl options _modSummary m = do
case parseLocs (foldMap T.pack options) of
Left err -> error err
Right [] -> pure m
Right locs -> do
dflags <- GHC.getDynFlags
debug $ GHC.showPpr dflags (GHC.hpm_module m)
debug "===>"
(hpm_module', locs_found) <-
runStateT (transform locs dflags (GHC.hpm_module m)) 0
if locs_found == length locs
then do
debug $ show locs
debug $ GHC.showPpr dflags (hpm_module')
let module' = m {GHC.hpm_module = hpm_module'}
return module'
else do
-- Later, we can collect the offending locations instead of
-- simply counting, and emit a more useful error message.
error "There is a question-mark used in a non-statement position!"
debug :: MonadIO m => String -> m ()
-- debug = liftIO . putStrLn
debug _ = pure ()
transform ::
[Loc]
-> GHC.DynFlags
-> GHC.Located (HsModule GhcPs)
-> StateT Int GHC.Hsc (GHC.Located (HsModule GhcPs))
transform locs dflags = SYB.everywhereM (SYB.mkM (transformDo dflags locs))
transformDo ::
GHC.DynFlags
-> [Loc]
-> LHsExpr GhcPs
-> StateT Int GHC.Hsc (LHsExpr GhcPs)
transformDo dflags locs =
\case
(L l (HsDo xdo DoExpr (L l' stmts@(_:_)))) -> do
stmts' <- transformStmts dflags locs stmts
pure (L l (HsDo xdo DoExpr (L l' stmts')))
e -> pure e
transformStmts ::
GHC.DynFlags
-> [Loc]
-> [LStmt GhcPs (LHsExpr GhcPs)]
-> StateT Int GHC.Hsc [LStmt GhcPs (LHsExpr GhcPs)]
transformStmts _ _ [] = pure []
transformStmts dflags locs (current:rest)
| stmtIsEarly locs current = do
modify' (+1)
stmts <- transformStmts dflags locs rest
pure (transformStmt current stmts)
| otherwise = fmap (current :) (transformStmts dflags locs rest)
transformStmt ::
LStmt GhcPs (LHsExpr GhcPs)
-> [LStmt GhcPs (LHsExpr GhcPs)]
-> [LStmt GhcPs (LHsExpr GhcPs)]
transformStmt (L stmtloc current) rest =
case current of
BodyStmt x lexpr l r ->
[ L stmtloc
(BodyStmt
x
(L GHC.noSrcSpan
(HsApp
NoExt
(L GHC.noSrcSpan
(HsApp
NoExt
(L GHC.noSrcSpan
(HsVar NoExt (L GHC.noSrcSpan earlyThenName)))
lexpr))
(L GHC.noSrcSpan (HsDo NoExt DoExpr (L GHC.noSrcSpan rest)))))
l
r)
]
BindStmt x lpat lexpr l r ->
[ L stmtloc
(BodyStmt
x
(L GHC.noSrcSpan
(HsApp
NoExt
(L GHC.noSrcSpan
(HsApp
NoExt
(L GHC.noSrcSpan
(HsVar NoExt (L GHC.noSrcSpan earlyName)))
lexpr))
(makeLambda
lpat
(L GHC.noSrcSpan
(HsDo NoExt DoExpr (L GHC.noSrcSpan rest))))))
l
r)
]
_ -> L stmtloc current : rest
-- | Making a lambda took me like 15 minutes of endless types. So this
-- is in a function.
makeLambda :: LPat GhcPs -> LHsExpr GhcPs -> LHsExpr GhcPs
makeLambda lpat lexpr =
L GHC.noSrcSpan
(HsLam
NoExt
(MG
NoExt
(L GHC.noSrcSpan
[ L GHC.noSrcSpan
(Match
NoExt
LambdaExpr
[lpat]
(GRHSs
NoExt
[L GHC.noSrcSpan (GRHS NoExt [] lexpr)]
(L GHC.noSrcSpan (EmptyLocalBinds NoExt))))
])
GHC.Generated))
stmtIsEarly :: [Loc] -> LStmt GhcPs (LHsExpr GhcPs) -> Bool
stmtIsEarly locs (L l BindStmt {}) = any (flip srcSpanFollowedBy l) locs
stmtIsEarly locs (L l BodyStmt {}) = any (flip srcSpanFollowedBy l) locs
stmtIsEarly _ _ = False
--------------------------------------------------------------------------------
-- Names
earlyName :: GHC.RdrName
earlyName = GHC.mkQual OccName.varName ("Control.Early","early")
earlyThenName :: GHC.RdrName
earlyThenName = GHC.mkQual OccName.varName ("Control.Early","earlyThen")
--------------------------------------------------------------------------------
-- Locations
srcSpanFollowedBy :: Loc -> SrcSpan -> Bool
srcSpanFollowedBy (Loc line col) sp =
case sp of
RealSrcSpan s -> srcSpanEndLine s == line + 1 && srcSpanEndCol s == col
_ -> False
data Loc = Loc
{ line, col :: !Int
} deriving (Eq, Ord, Show)
parseLocs :: Text -> Either String [Loc]
parseLocs =
mapM
((\case
[x, y] -> do
line <- readEither (T.unpack x)
col <- readEither (T.unpack y)
pure (Loc {line, col})
_ -> Left "Expected line:col pattern for input.") .
T.splitOn ":") .
T.splitOn ","