{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module Language.Haskell.GHC.ExactPrint.Preprocess
(
stripLinePragmas
, getCppTokensAsComments
, getPreprocessedSrcDirect
, readFileGhc
, CppOptions(..)
, defaultCppOptions
) where
import qualified Bag as GHC
import qualified DriverPhases as GHC
import qualified DriverPipeline as GHC
import qualified DynFlags as GHC
import qualified ErrUtils as GHC
import qualified FastString as GHC
import qualified GHC as GHC hiding (parseModule)
import qualified HscTypes as GHC
import qualified Lexer as GHC
import qualified MonadUtils as GHC
import qualified SrcLoc as GHC
import qualified StringBuffer as GHC
import SrcLoc (mkSrcSpan, mkSrcLoc)
import FastString (mkFastString)
import Control.Exception
import Data.List hiding (find)
import Data.Maybe
#if __GLASGOW_HASKELL__ <= 800
import Language.Haskell.GHC.ExactPrint.GhcInterim (commentToAnnotation)
#endif
import Language.Haskell.GHC.ExactPrint.Types
import Language.Haskell.GHC.ExactPrint.Utils
import qualified Data.Set as Set
{-# ANN module ("HLint: ignore Eta reduce" :: String) #-}
{-# ANN module ("HLint: ignore Redundant do" :: String) #-}
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
data CppOptions = CppOptions
{ cppDefine :: [String]
, cppInclude :: [FilePath]
, cppFile :: [FilePath]
}
defaultCppOptions :: CppOptions
defaultCppOptions = CppOptions [] [] []
stripLinePragmas :: String -> (String, [Comment])
stripLinePragmas = unlines' . unzip . findLines . lines
where
unlines' (a, b) = (unlines a, catMaybes b)
findLines :: [String] -> [(String, Maybe Comment)]
findLines = zipWith checkLine [1..]
checkLine :: Int -> String -> (String, Maybe Comment)
checkLine line s
| "{-# LINE" `isPrefixOf` s =
let (pragma, res) = getPragma s
size = length pragma
mSrcLoc = mkSrcLoc (mkFastString "LINE")
ss = mkSrcSpan (mSrcLoc line 1) (mSrcLoc line (size+1))
in (res, Just $ mkComment pragma ss)
| "#!" `isPrefixOf` s =
let mSrcLoc = mkSrcLoc (mkFastString "SHEBANG")
ss = mkSrcSpan (mSrcLoc line 1) (mSrcLoc line (length s))
in
("",Just $ mkComment s ss)
| otherwise = (s, Nothing)
getPragma :: String -> (String, String)
getPragma [] = error "Input must not be empty"
getPragma s@(x:xs)
| "#-}" `isPrefixOf` s = ("#-}", " " ++ drop 3 s)
| otherwise =
let (prag, remline) = getPragma xs
in (x:prag, ' ':remline)
getCppTokensAsComments :: GHC.GhcMonad m
=> CppOptions
-> FilePath
-> m [Comment]
getCppTokensAsComments cppOptions sourceFile = do
source <- GHC.liftIO $ GHC.hGetStringBuffer sourceFile
let startLoc = GHC.mkRealSrcLoc (GHC.mkFastString sourceFile) 1 1
(_txt,strSrcBuf,flags2) <- getPreprocessedSrcDirectPrim cppOptions sourceFile
directiveToks <- GHC.liftIO $ getPreprocessorAsComments sourceFile
nonDirectiveToks <- tokeniseOriginalSrc startLoc flags2 source
case GHC.lexTokenStream strSrcBuf startLoc flags2 of
GHC.POk _ ts ->
do
let toks = GHC.addSourceToTokens startLoc source ts
cppCommentToks = getCppTokens directiveToks nonDirectiveToks toks
return $ filter goodComment
#if __GLASGOW_HASKELL__ > 800
$ map (tokComment . GHC.commentToAnnotation . fst) cppCommentToks
#else
$ map (tokComment . commentToAnnotation . fst) cppCommentToks
#endif
#if __GLASGOW_HASKELL__ >= 804
GHC.PFailed _ sspan err -> parseError flags2 sspan err
#else
GHC.PFailed sspan err -> parseError flags2 sspan err
#endif
goodComment :: Comment -> Bool
goodComment (Comment "" _ _) = False
goodComment _ = True
getCppTokens ::
[(GHC.Located GHC.Token, String)]
-> [(GHC.Located GHC.Token, String)]
-> [(GHC.Located GHC.Token, String)]
-> [(GHC.Located GHC.Token, String)]
getCppTokens directiveToks origSrcToks postCppToks = toks
where
locFn (GHC.L l1 _,_) (GHC.L l2 _,_) = compare l1 l2
m1Toks = mergeBy locFn postCppToks directiveToks
origSpans = map (\(GHC.L l _,_) -> l) origSrcToks
m1Spans = map (\(GHC.L l _,_) -> l) m1Toks
missingSpans = Set.fromList origSpans Set.\\ Set.fromList m1Spans
missingToks = filter (\(GHC.L l _,_) -> Set.member l missingSpans) origSrcToks
missingAsComments = map mkCommentTok missingToks
where
mkCommentTok :: (GHC.Located GHC.Token,String) -> (GHC.Located GHC.Token,String)
mkCommentTok (GHC.L l _,s) = (GHC.L l (GHC.ITlineComment s),s)
toks = mergeBy locFn directiveToks missingAsComments
tokeniseOriginalSrc ::
GHC.GhcMonad m
=> GHC.RealSrcLoc -> GHC.DynFlags -> GHC.StringBuffer
-> m [(GHC.Located GHC.Token, String)]
tokeniseOriginalSrc startLoc flags buf = do
let src = stripPreprocessorDirectives buf
case GHC.lexTokenStream src startLoc flags of
GHC.POk _ ts -> return $ GHC.addSourceToTokens startLoc src ts
#if __GLASGOW_HASKELL__ >= 804
GHC.PFailed _ sspan err -> parseError flags sspan err
#else
GHC.PFailed sspan err -> parseError flags sspan err
#endif
stripPreprocessorDirectives :: GHC.StringBuffer -> GHC.StringBuffer
stripPreprocessorDirectives buf = buf'
where
srcByLine = lines $ sbufToString buf
noDirectivesLines = map (\line -> if line /= [] && head line == '#' then "" else line) srcByLine
buf' = GHC.stringToStringBuffer $ unlines noDirectivesLines
sbufToString :: GHC.StringBuffer -> String
sbufToString sb@(GHC.StringBuffer _buf len _cur) = GHC.lexemeToString sb len
getPreprocessedSrcDirect :: (GHC.GhcMonad m)
=> CppOptions
-> FilePath
-> m (String, GHC.DynFlags)
getPreprocessedSrcDirect cppOptions src =
(\(s,_,d) -> (s,d)) <$> getPreprocessedSrcDirectPrim cppOptions src
getPreprocessedSrcDirectPrim :: (GHC.GhcMonad m)
=> CppOptions
-> FilePath
-> m (String, GHC.StringBuffer, GHC.DynFlags)
getPreprocessedSrcDirectPrim cppOptions src_fn = do
hsc_env <- GHC.getSession
let dfs = GHC.hsc_dflags hsc_env
new_env = hsc_env { GHC.hsc_dflags = injectCppOptions cppOptions dfs }
(dflags', hspp_fn) <-
GHC.liftIO $ GHC.preprocess new_env (src_fn, Just (GHC.Cpp GHC.HsSrcFile))
buf <- GHC.liftIO $ GHC.hGetStringBuffer hspp_fn
txt <- GHC.liftIO $ readFileGhc hspp_fn
return (txt, buf, dflags')
injectCppOptions :: CppOptions -> GHC.DynFlags -> GHC.DynFlags
injectCppOptions CppOptions{..} dflags =
foldr addOptP dflags (map mkDefine cppDefine ++ map mkIncludeDir cppInclude ++ map mkInclude cppFile)
where
mkDefine = ("-D" ++)
mkIncludeDir = ("-I" ++)
mkInclude = ("-include" ++)
addOptP :: String -> GHC.DynFlags -> GHC.DynFlags
addOptP f = alterSettings (\s -> s { GHC.sOpt_P = f : GHC.sOpt_P s})
alterSettings :: (GHC.Settings -> GHC.Settings) -> GHC.DynFlags -> GHC.DynFlags
alterSettings f dflags = dflags { GHC.settings = f (GHC.settings dflags) }
getPreprocessorAsComments :: FilePath -> IO [(GHC.Located GHC.Token, String)]
getPreprocessorAsComments srcFile = do
fcontents <- readFileGhc srcFile
let directives = filter (\(_lineNum,line) -> line /= [] && head line == '#')
$ zip [1..] (lines fcontents)
let mkTok (lineNum,line) = (GHC.L l (GHC.ITlineComment line),line)
where
start = GHC.mkSrcLoc (GHC.mkFastString srcFile) lineNum 1
end = GHC.mkSrcLoc (GHC.mkFastString srcFile) lineNum (length line)
l = GHC.mkSrcSpan start end
let toks = map mkTok directives
return toks
parseError :: GHC.DynFlags -> GHC.SrcSpan -> GHC.MsgDoc -> m b
parseError dflags sspan err = do
throw $ GHC.mkSrcErr (GHC.unitBag $ GHC.mkPlainErrMsg dflags sspan err)
readFileGhc :: FilePath -> IO String
readFileGhc file = do
buf@(GHC.StringBuffer _ len _) <- GHC.hGetStringBuffer file
return (GHC.lexemeToString buf len)
mergeBy :: (a -> a -> Ordering) -> [a] -> [a] -> [a]
mergeBy _cmp [] ys = ys
mergeBy _cmp xs [] = xs
mergeBy cmp (allx@(x:xs)) (ally@(y:ys))
| (x `cmp` y) <= EQ = x : mergeBy cmp xs ally
| otherwise = y : mergeBy cmp allx ys