cparsing-0.1.0.0: Text/Preprocess/Parser.hs
{-# LANGUAGE FlexibleContexts, KindSignatures #-}
-- | Generic components for a preprocessor. Support for macro definitions with or without parameters, comments, file includes, conditional compilation.
module Text.Preprocess.Parser where
import Text.Parsec hiding (label)
import Text.Preprocess.Rewrites
import Text.ParserCombinators.Parsec.Pos
import Text.Parsec.ExtraCombinators
import Control.Monad.Identity
import Control.Monad.Trans
import Control.Applicative hiding ((<|>), many, optional)
import Data.List
import Data.Maybe
import Data.Either
import Debug.Trace
-- | Contains the specification of the preprocessor
data Preprocessor s u appl (m :: * -> *)
= Preprocessor { macroDef :: ParsecT s (u, PPState s u appl) m (MacroDef appl)
, comments :: ParsecT s (u, PPState s u appl) m ()
, -- | Should parse all occasions where a macro replacement could occur.
macroAppl :: ParsecT s (u, PPState s u appl) m appl
, includeDirective :: FilePath -> ParsecT s (u, PPState s u appl) m (String, FilePath)
, condDirective :: PPState s u appl -> ParsecT s (u, PPState s u appl) m [PPRes]
, failDirective :: ParsecT s (u, PPState s u appl) m ()
, identToken :: ParsecT s (u, PPState s u appl) m String
}
-- | A default preprocessor with no parsers defined.
defaultPreprocessor :: Preprocessor s u appl m
defaultPreprocessor = Preprocessor { macroDef = fail "No macros defined"
, comments = fail "No comments defined"
, macroAppl = fail "No macro application defined"
, includeDirective = fail "No include directive defined"
, condDirective = fail "No cond directive defined"
, failDirective = fail "No failure directive defined"
, identToken = fail "No identifier token defined"
}
-- | All information about a macro definition
--
-- Contains it's name (for debugging), the way macro definition conflicts are resolved and a function
-- that decides if a possible source code can be rewritten by this macro and does the rewrite if so.
data MacroDef appl
= MacroDef { name :: String
, resolveConflict :: MacroDef appl -> Maybe (MacroDef appl)
, macroExpand :: appl -> Maybe (Either String [PPRes])
}
instance Show (MacroDef appl) where
show md = "MacroDef { name = " ++ name md ++ ", ... }"
-- | Wraps a part of a generated source code with the information that it is to be preprocessed again or not.
data PPRes = PPAgain { fromPPRes :: String }
| NoPP { fromPPRes :: String }
-- | Preprocessing state
data PPState s u appl
= PPState { defs :: [MacroDef appl] -- ^ Defined macros
, rewrites :: RewriteSet -- ^ Performed rewrites
, genPos :: SourcePos -- ^ Current position in the generated file,
-- to keep track of destination position of rewrites
} deriving Show
-- | Creates an initial preprocessing state for parsing a file with the given name.
initState :: FilePath -> PPState s u appl
initState fileName = PPState [] emptyRewSet (initialPos fileName)
-- | The parser type that includes preprocessing state in the user state
type PPParserT s u appl m a
= ParsecT s (u, PPState s u appl) m a
-- | Instructs the preprocessor to modify the macro definition table
modifyDefs :: (Monad m) => ([MacroDef appl] -> Either String [MacroDef appl]) -> PPParserT s u appl m ()
modifyDefs f = do (u,s) <- getState
case f (defs s) of Right defs' -> putState (u,s { defs = defs' })
Left err -> fail err
-- | Registers a rewrite, so it can be undone when the original source is queried
registerRewrite :: (Monad m) => SourcePos -> String -> String -> RewriteTrigger -> PPParserT s u appl m ()
registerRewrite origPos origStr toStr trig
= modifyState $ \(u,s) -> (u, s { rewrites = addRewrite (createRewrite origPos origStr (genPos s) toStr trig) (rewrites s) })
-- | Updates the generated position
moveGenPos :: (Monad m) => String -> PPParserT s u appl m ()
moveGenPos s = modifyState $ \(u,st) -> (u,st { genPos = updatePosString (genPos st) s })
-- | Runs the preprocessor on a complete source file
preprocessSource :: Monad m => Preprocessor String u appl m
-> PPParserT String u appl m (String, (u, PPState String u appl))
preprocessSource pp = (,) <$> preprocess pp <* eof <*> getState
-- | Preprocesses the source, returns preprocessing state.
-- Runs while the source is valid for preprocessing.
-- Can be used to capture branches of conditionally compiled source code.
preprocess :: Monad m => Preprocessor String u appl m
-> PPParserT String u appl m String
preprocess pp
= do dir <- sourceName <$> getPosition
(concat . concat) <$> many ( try $ do
pos <- getPosition
((prepRes, file, trig), src) <- captureInputStr $
(comments pp >> return ([], dir, CommentRemoval))
<|> (parseDefine pp >> return ([], dir, MacroDefinition))
<|> ( do (r, newFile) <- includeDirective pp dir
return ([PPAgain r], newFile, IncludeTriggered) )
<|> (try (replaceDefine pp) >>= \ppres -> return (ppres, dir, MacroRewrite))
<|> (identToken pp >>= \str -> return ([NoPP str], dir, MacroRewrite))
<|> (do st <- snd <$> getState
ppres <- condDirective pp st
return (ppres, dir, IfDefTriggered))
<|> (failDirective pp *> undefined)
<|> (anyChar >>= \c -> return ([NoPP [c]], dir, MacroRewrite))
let strRes = concatMap fromPPRes prepRes
when (src /= strRes) $ do
registerRewrite pos src strRes trig
moveGenPos strRes
mapM (\r -> case r of NoPP s -> return s
PPAgain s -> runPreprocAgain file s) prepRes
)
where runPreprocAgain file s = do
st <- getState
pos <- getPosition
res <- lift $ runParserT ( do setPosition (setSourceName pos file)
preprocessSource pp
) st file s
case res of
Right (resTxt, resSt) ->
do setState resSt
return resTxt
Left err -> fail (show err)
-- | Parses a define directive
parseDefine :: Monad m => Preprocessor String u appl m
-> PPParserT String u appl m ()
parseDefine pp
= do (macro, src) <- captureInputStr (macroDef pp)
modifyDefs (\defs -> case find (\m -> name m == name macro) defs of
Just other -> case resolveConflict macro other of
Just newMacro -> Right $ newMacro : defs
Nothing -> Left "Macro redefined"
Nothing -> Right $ macro : defs )
-- | Replaces a name of a define with its body
replaceDefine :: Monad m => Preprocessor String u appl m
-> PPParserT String u appl m [PPRes]
replaceDefine pp = try $ do st <- getState
appl <- macroAppl pp
case partitionEithers $ catMaybes $ map (\md -> macroExpand md appl) (defs $ snd st) of
(s:_, _) -> error s
([], []) -> fail ""
([], a:_) -> return a
-- | Creates a line comment that starts from the given symbol to the end of line
lineComment :: Monad m => String -> PPParserT String u appl m ()
lineComment str = do try (string str)
skip $ manyTill anyChar (skip (string "\n") <|> eof)
-- | Creates a block comment
blockComment :: Monad m => String -> String -> PPParserT String u appl m ()
blockComment start end = skip $ do string start
manyTill anyChar (try (string end))