{-# LANGUAGE LambdaCase, OverloadedStrings, ScopedTypeVariables, ViewPatterns #-} -- | Implement the logic of CPP directives (commands prefixed with an -- octothorpe). module Hpp.Directive (directive, macroExpansion) where import Control.Monad (unless) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except import Control.Monad.Trans.State.Strict (StateT) import Hpp.Conditional (dropBranch, takeBranch) import Hpp.Config (curFileName, curFileNameF) import Hpp.Env (lookupKey, deleteKey, insertPair) import Hpp.Expansion (expandLineState) import Hpp.Expr (evalExpr, parseExpr) import Hpp.Macro (parseDefinition) import Hpp.Preprocessing (prepareInput) import Hpp.StringSig (unquote, toChars) import Hpp.Tokens (newLine, notImportant, trimUnimportant, detokenize, isImportant, Token(..)) import Hpp.Types import Hpp.Parser (replace, await, insertInputSegment, takingWhile, droppingWhile, onInputSegment, evalParse, onElements, awaitJust, ParserT, Parser) import Text.Read (readMaybe) import Prelude hiding (String) -- | Returns everything up to the next newline. The newline character -- itself is consumed. takeLine :: (Monad m, HasError m, HasHppState m) => Parser m [TOKEN] [TOKEN] takeLine = (onElements $ do ln <- takingWhile (not . newLine) eat <- awaitJust "takeLine" -- Eat the newline character case eat of Other "\n" -> return () wat -> error $ "Expected newline: "++show wat++" after "++show ln return ln) <* (lineNum %= (+1)) dropLine :: (Monad m, HasError m, HasHppState m) => Parser m [TOKEN] () dropLine = do onElements $ do droppingWhile (not . newLine) eat <- awaitJust "dropLine" -- Eat the newline character case eat of Other "\n" -> return () wat -> error $ "Expected dropped newline: "++show wat lineNum %= (+1) droppingSpaces ::(Monad m) => ParserT m src TOKEN () droppingSpaces = droppingWhile notImportant -- | Run a Stream with a configuration for a new file. streamNewFile :: (Monad m, HasHppState m) => FilePath -> [[TOKEN]] -> Parser m [TOKEN] () streamNewFile fp s = do (oldCfg,oldLine) <- do st <- getState let cfg = hppConfig st cfg' = cfg { curFileNameF = pure fp } ln = hppLineNum st -- NOTE: We should *NOT* use a the config lens here -- because it will mutate the directory which -- we *don't* want in this instance. setState (st {hppConfig = cfg', hppLineNum = 1}) return (cfg, ln) insertInputSegment s (getState >>= setState . setL lineNum oldLine . setL config oldCfg) -- | Handle preprocessor directives (commands prefixed with an octothorpe). directive :: forall m. (Monad m, HasError m, HasHppState m, HasEnv m) => HppT [String] (Parser m [TOKEN]) Bool directive = lift (onElements (awaitJust "directive")) >>= aux where aux :: TOKEN -> HppT [String] (Parser m [TOKEN]) Bool aux (Important cmd) = case cmd of "pragma" -> True <$ lift dropLine -- Ignored "define" -> True <$ (lift $ fmap parseDefinition takeLine >>= \case Nothing -> use lineNum >>= throwError . BadMacroDefinition Just def -> env %= insertPair def) "undef" -> do name <- lift . onElements $ do droppingWhile (not . isImportant) name <- awaitJust "undef" >>= \case Important n -> return n _ -> error "undef directive got Other token" return name lift dropLine env %= deleteKey name return True "include" -> True <$ includeAux hppReadFile "include_next" -> True <$ includeAux hppReadNext "line" -> do lift (onElements droppingSpaces) toks <- lift (init <$> expandLineState) case toks of Important (toChars -> n):optFile -> case readMaybe n of Nothing -> use lineNum >>= throwError . flip BadLineArgument n Just ln' -> do unless (null optFile) $ do let fn = toChars . unquote . detokenize . dropWhile (not . isImportant) $ optFile config %= (\cfg -> cfg { curFileNameF = pure fn }) lineNum .= ln' return True _ -> use lineNum >>= throwError . flip BadLineArgument (toChars (detokenize toks)) "ifdef" -> do toks <- lift (onElements droppingSpaces >> takeLine) ln <- use lineNum case takeWhile isImportant toks of [Important t] -> lookupMacro t >>= \case Nothing -> lift dropBranch Just _ -> lift (onInputSegment (takeBranch ln)) -- (takeBranch ln >>= precede) _ -> throwError . UnknownCommand ln $ "ifdef "++ toChars (detokenize toks) return True "ifndef" -> do toks <- lift (onElements droppingSpaces >> takeLine) ln <- use lineNum case takeWhile isImportant toks of [Important t] -> lookupMacro t >>= \case Nothing -> lift (onInputSegment (takeBranch ln)) -- takeBranch ln >>= precede) Just _ -> lift dropBranch _ -> throwError . UnknownCommand ln $ "ifndef "++ toChars (detokenize toks) return True "else" -> True <$ lift dropLine "if" -> True <$ ifAux "elif" -> True <$ ifAux "endif" -> True <$ lift dropLine "error" -> do toks <- lift (onElements droppingSpaces >> takeLine) ln <- subtract 1 <$> use lineNum curFile <- curFileName <$> use config let tokStr = toChars (detokenize toks) throwError $ UserError ln (tokStr++" ("++curFile++")") "warning" -> True <$ lift dropLine -- warnings not yet supported t -> do toks <- lift takeLine ln <- subtract 1 <$> use lineNum throwError $ UnknownCommand ln (toChars (detokenize (Important t:toks))) aux _ = error "Impossible unimportant directive" includeAux :: (LineNum -> FilePath -> HppT src (Parser m [TOKEN]) [String]) -> HppT src (Parser m [TOKEN]) () includeAux readFun = do fileName <- lift (toChars . detokenize . trimUnimportant . init <$> expandLineState) ln <- use lineNum src <- prepareInput <*> readFun ln fileName lineNum .= ln+1 lift (streamNewFile (unquote fileName) src) {- SPECIALIZE includeAux :: (LineNum -> FilePath -> HppT [String] (Parser (StateT HppState (ExceptT Error IO)) [TOKEN]) [String]) -> HppT [String] (Parser (StateT HppState (ExceptT Error IO)) [TOKEN]) () #-} ifAux = do toks <- lift (onElements droppingSpaces >> takeLine) e <- use env ln <- use lineNum lineNum .= ln - 1 -- takeLine incremented the line count ex <- lift (lift (evalParse expandLineState [squashDefines e toks])) let res = evalExpr <$> parseExpr (map (fmap toChars) ex) lineNum .= ln if maybe False (/= 0) res then lift (onInputSegment (takeBranch ln)) -- (takeBranch ln >>= precede) else lift dropBranch {-# SPECIALIZE directive :: HppT [String] (Parser (StateT HppState (ExceptT Error IO)) [TOKEN]) Bool #-} -- | We want to expand macros in expressions that must be evaluated -- for conditionals, but we want to take special care when dealing -- with the meta @defined@ operator of the expression language that is -- a predicate on the evaluation environment. squashDefines :: Env -> [TOKEN] -> [TOKEN] squashDefines _ [] = [] squashDefines env' (Important "defined" : ts) = go ts where go (t@(Other _) : ts') = t : go ts' go (t@(Important "(") : ts') = t : go ts' go (Important t : ts') = case lookupKey t env' of Nothing -> Important "0" : squashDefines env' ts' -- Just (_,env'') -> Important "1" : squashDefines env'' ts' Just _ -> Important "1" : squashDefines env' ts' go [] = [] squashDefines env' (t : ts) = t : squashDefines env' ts -- | Expands an input line producing a stream of output lines. macroExpansion :: (Monad m, HasHppState m, HasError m, HasEnv m) => HppT [String] (Parser m [TOKEN]) (Maybe [TOKEN]) macroExpansion = do lift await >>= \case Nothing -> return Nothing Just ln -> -- when (not (all isSpace (detokenize ln))) -- (trace ("macro expand: "++detokenize ln) (return ())) >> case dropWhile notImportant ln of [] -> Just ln <$ (lineNum %= (+1)) Important "#":rst -> do lift (replace (dropWhile notImportant rst)) processed <- directive if processed then macroExpansion else Just ln <$ lift takeLine _ -> lift (replace ln >> (Just <$> expandLineState)) <* (lineNum %= (+1))