packages feed

hpp 0.4.1 → 0.5.0

raw patch · 10 files changed

+905/−649 lines, 10 filesdep ~basedep ~transformersPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, transformers

API changes (from Hackage documentation)

- Hpp: hppFileContents :: Config -> Env -> FilePath -> [String] -> IO (Either Error [String])
- Hpp: hppIO :: Config -> Env -> ([String] -> IO ()) -> [String] -> IO ()
- Hpp: hppReadFile :: Monad m => Int -> FilePath -> HppT src m src
- Hpp: type HppCaps t = (HasError t, HasHppState t, HasEnv t)
+ Hpp: HppOutput :: [FilePath] -> [ByteString] -> HppOutput
+ Hpp: [hppFilesRead] :: HppOutput -> [FilePath]
+ Hpp: [hppOutput] :: HppOutput -> [ByteString]
+ Hpp: addDefinition :: ByteString -> ByteString -> HppState -> Maybe HppState
+ Hpp: data HppOutput
+ Hpp: data HppState
+ Hpp: data HppT m a
+ Hpp: emptyHppState :: HppState
+ Hpp: initHppState :: Config -> Env -> HppState
+ Hpp: instance Control.Monad.Trans.Class.MonadTrans Hpp.HppT
+ Hpp: instance GHC.Base.Functor m => GHC.Base.Functor (Hpp.HppT m)
+ Hpp: instance GHC.Base.Monad m => GHC.Base.Applicative (Hpp.HppT m)
+ Hpp: instance GHC.Base.Monad m => GHC.Base.Monad (Hpp.HppT m)
+ Hpp: runHpp :: MonadIO m => HppState -> HppT m a -> ExceptT Error m (HppOutput, HppState)
+ Hpp: streamHpp :: MonadIO m => HppState -> ([ByteString] -> m ()) -> HppT m a -> ExceptT Error m ([FilePath], HppState)
+ Hpp.RunHpp: HppResult :: [FilePath] -> a -> HppResult a
+ Hpp.RunHpp: [hppFilesRead] :: HppResult a -> [FilePath]
+ Hpp.RunHpp: [hppResult] :: HppResult a -> a
+ Hpp.RunHpp: data HppResult a
+ Hpp.RunHpp: hppIO :: Config -> Env -> FilePath -> [String] -> IO (Either Error ([FilePath], [String]))
+ Hpp.RunHpp: hppIOSink :: Config -> Env -> ([String] -> IO ()) -> [String] -> IO [FilePath]
+ Hpp.RunHpp: parseDefinition :: [TOKEN] -> Maybe (String, Macro)
+ Hpp.RunHpp: preprocess :: (Monad m, HppCaps m) => [String] -> HppT [String] (Parser m [TOKEN]) ()
+ Hpp.RunHpp: runHpp :: forall m a src. (MonadIO m, HasHppState m) => (FilePath -> m src) -> (src -> m ()) -> HppT src m a -> m (Either (FilePath, Error) (HppResult a))
+ Hpp.RunHpp: type HppCaps t = (HasError t, HasHppState t, HasEnv t)
+ Hpp.Types: instance GHC.Exception.Exception Hpp.Types.Error
- Hpp: parseDefinition :: [TOKEN] -> Maybe (String, Macro)
+ Hpp: parseDefinition :: ByteString -> ByteString -> Maybe (ByteString, Macro)
- Hpp: preprocess :: (Monad m, HppCaps m) => [String] -> HppT [String] (Parser m [TOKEN]) ()
+ Hpp: preprocess :: Monad m => [ByteString] -> HppT m ()

Files

CHANGELOG.md view
@@ -1,3 +1,7 @@+# 0.5.0+- Redesigned library API+The `Hpp` module exports the main pieces. `Hpp.Env`, `Hpp.Types`, and `Hpp.Config` may be used for configuring the preprocessor.+ # 0.4.0  - Simplify the parsing machinery
README.md view
@@ -1,3 +1,4 @@+# The `hpp` Executable `hpp` is a Haskell pre-processor that is also a C90-compatible pre-processor (with the addition of a `--cpp` flag). It is packaged as both a library and an executable.@@ -30,3 +31,58 @@ ```  Note that you will need to ensure that the `hpp` executable is available in your build environment (e.g. you can add `hpp` as a `build-depends` in your `.cabal` file).++# The `hpp` Library++The `hpp` executable is a command-line interface to the `hpp` library. While the `hpp` package has been designed to have minimal dependencies beyond what the `GHC` compiler itself uses, it does include a few small, framework-free unit tests that demonstrate basic usage as a library. In the `testIf` example, we preprocess the `sourceIfDef` input with a starting definition equivalent to `#define FOO 1`. In `testArith1`, we exercise basic integer arithmetic and comparison. The `hppHelper` function shows how to run your source input through the preprocessor: `runHpp initialState (preproces mySource)`.++```haskell+{-# LANGUAGE LambdaCase, OverloadedStrings #-}+import Control.Monad.Trans.Except+import Data.ByteString.Char8 (ByteString)+import Data.Maybe (fromMaybe)+import Data.Monoid ((<>))+import Hpp+import System.Exit++sourceIfdef :: [ByteString]+sourceIfdef = [ "#ifdef FOO"+              , "x = 42"+              , "#else"+              , "x = 99"+              , "#endif" ]++sourceArith1 :: ByteString -> [ByteString]+sourceArith1 s = [ "#define x 3"+                 , "#if 5 + x > " <> s+                 , "yay"+                 , "#else"+                 , "boo"+                 , "#endif" ]++hppHelper :: HppState -> [ByteString] -> [ByteString] -> IO Bool+hppHelper st src expected = runExceptT (runHpp st (preprocess src)) >>= \case+  Left e -> putStrLn ("Error running hpp: " ++ show e) >> return False+  Right (res, _) -> if hppOutput res == expected+                       then return True+                       else do putStr ("Expected "++show expected++", got")+                               print (hppOutput res)+                               return False++testElse :: IO Bool+testElse = hppHelper emptyHppState sourceIfdef ["x = 99\n","\n"]++testIf :: IO Bool+testIf = hppHelper (fromMaybe (error "Preprocessor definition did not parse")+                              (addDefinition "FOO" "1" emptyHppState))+                   sourceIfdef+                   ["x = 42\n","\n"]++testArith1 :: IO Bool+testArith1 = (&&) <$> hppHelper emptyHppState (sourceArith1 "7") ["yay\n","\n"]+                  <*> hppHelper emptyHppState (sourceArith1 "8") ["boo\n","\n"]++main :: IO ()+main = do results <- sequenceA [testElse, testIf, testArith1]+          if and results then exitWith ExitSuccess else exitWith (ExitFailure 1)+```
hpp.cabal view
@@ -1,5 +1,5 @@ name:                hpp-version:             0.4.1+version:             0.5.0 synopsis:            A Haskell pre-processor description:         See the README for usage examples license:             BSD3@@ -26,11 +26,12 @@                        Hpp.Expansion,                        Hpp.Expr,                        Hpp.Parser,+                       Hpp.RunHpp,                        Hpp.String,                        Hpp.StringSig,                        Hpp.Tokens,                        Hpp.Types-  build-depends:       base >=4.8 && <4.11, directory, time >=1.5, filepath,+  build-depends:       base >=4.8 && <4.12, directory, time >=1.5, filepath,                        transformers >=0.4, bytestring, bytestring-trie, ghc-prim    if impl(ghc < 8)@@ -42,7 +43,13 @@  executable hpp   main-is:             src/Main.hs-  build-depends:       hpp, base >=4.8 && <4.11, directory, time >=1.5, filepath+  build-depends:       hpp, base >=4.8 && <4.12, directory, time >=1.5, filepath   hs-source-dirs:      .   default-language:    Haskell2010   ghc-options: -Wall++test-suite aslib+  type:       exitcode-stdio-1.0+  main-is:    tests/AsLib.hs+  build-depends: base, hpp, transformers, bytestring+  default-language:    Haskell2010
src/Hpp.hs view
@@ -1,647 +1,99 @@-{-# LANGUAGE BangPatterns, ConstraintKinds, LambdaCase, OverloadedStrings,-             ScopedTypeVariables, TupleSections, ViewPatterns #-}+{-# LANGUAGE GeneralizedNewtypeDeriving, TupleSections #-} -- | Front-end interface to the pre-processor. module Hpp (parseDefinition, preprocess,-            hppReadFile, hppIO, HppCaps, hppFileContents) where-import Control.Arrow (first)-import Control.Monad (unless)-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Except+            T.HppState, emptyHppState, initHppState, addDefinition,+            runHpp, streamHpp, HppT, HppOutput(..)) where import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Trans.State.Strict (StateT, evalStateT)-import Data.Char (isSpace)+import Control.Monad.Trans.Class (lift, MonadTrans)+import Control.Monad.Trans.Except (ExceptT, throwE)+import Control.Monad.Trans.State.Strict (StateT, runStateT)+import Data.ByteString.Char8 (ByteString) import Data.IORef import Data.Maybe (fromMaybe)-import Data.Monoid ((<>))-import Data.String (fromString)-import Hpp.Config (Config, curFileNameF, curFileName, includePaths,-                   eraseCComments, spliceLongLines, inhibitLinemarkers,-                   replaceTrigraphs)-import Hpp.Env (deleteKey, insertPair, lookupKey)-import Hpp.Expansion (expandLine)-import Hpp.Expr (evalExpr, parseExpr)-import Hpp.Parser (Parser, ParserT, replace, await, awaitJust, droppingWhile,-                   precede, takingWhile, insertInputSegment, onElements,-                   evalParse, onInputSegment)-import Hpp.StringSig-import Hpp.Tokens (Token(..), importants, isImportant, newLine, trimUnimportant,-                   detokenize, notImportant, tokenize, skipLiteral)-import Hpp.Types-import System.Directory (doesFileExist)-import System.FilePath ((</>))-import Text.Read (readMaybe)-import Prelude hiding (String)-import qualified Prelude as P---- * Trigraphs---- | The first component of each pair represents the end of a known--- trigraph sequence (each trigraph begins with two consecutive--- question marks (@"??"@). The second component is the--- single-character equivalent that we substitute in for the trigraph.-trigraphs :: [(Char, Char)]-trigraphs = [ ('=', '#')-            , ('/', '\\')-            , ('\'', '^')-            , ('(', '[')-            , (')', ']')-            , ('!', '|')-            , ('<', '{')-            , ('>', '}')-            , ('-', '~') ]--trigraphReplacement :: Stringy s => s -> s-trigraphReplacement s = aux (breakOn [("??", ())] s)-  where aux Nothing = s-        aux (Just (_, pre, pos)) =-          case uncons pos of-            Nothing -> pre <> "??"-            Just (c,t) ->-              case lookup c trigraphs of-                Just c' -> snoc pre c' <> trigraphReplacement t-                Nothing -> snoc pre '?' <> trigraphReplacement (cons '?' pos)---- * Line Splicing---- | If a line ends with a backslash, it is prepended to the following--- the line.-lineSplicing :: Stringy s => [s] -> [s]-lineSplicing = go id-  where go acc [] = [acc mempty]-        go acc (ln:lns) = case unsnoc ln of-                            Nothing -> acc mempty : go id lns-                            Just (ini, '\\') -> go (acc . (ini<>)) lns-                            Just _ -> acc ln : go id lns-{-# INLINE lineSplicing #-}---- * C Comments--breakBlockCommentStart :: Stringy s => s -> Maybe (s, s)-breakBlockCommentStart s =-  case breakCharOrSub '"' "/*" s of-    NoMatch -> Nothing-    CharMatch pre pos -> let (lit, rest) = skipLiteral pos-                         in first ((pre <> lit) <>) <$>-                            breakBlockCommentStart rest-    SubMatch pre pos -> Just (pre, pos)--breakBlockCommentEnd :: Stringy s => s -> Maybe s-breakBlockCommentEnd s =-  case breakCharOrSub '"' "*/" s of-    NoMatch -> Nothing-    CharMatch _ pos -> let (_, rest) = skipLiteral pos-                       in breakBlockCommentEnd rest-    SubMatch _ pos -> Just pos--dropOneLineBlockComments :: Stringy s => s -> s-dropOneLineBlockComments s =-  case breakCharOrSub '"' "/*"s of-    NoMatch -> s-    CharMatch pre pos ->-      let (lit,rest) = skipLiteral pos-      in snoc pre '"' <> lit <> dropOneLineBlockComments rest-    SubMatch pre pos ->-      case breakOn [("*/", ())] pos of-        Nothing -> pre <> "/*"-        Just (_,_,pos') -> snoc pre ' ' <> dropOneLineBlockComments pos'--removeMultilineComments :: Stringy s => Int -> [s] -> [s]-removeMultilineComments !lineStart = goStart lineStart-  where goStart _ [] = []-        goStart !curLine (ln:lns) =-          case breakBlockCommentStart ln of-            Nothing -> ln : goStart (curLine+1) lns-            Just (pre,_) -> goEnd (curLine+1) pre lns-        goEnd _ _ [] = error "Unmatched /*"-        goEnd !curLine pre (ln:lns) =-          case breakBlockCommentEnd ln of-            Nothing -> goEnd (curLine+1) pre lns-            Just pos-              | sall isSpace (pre<>pos) ->-                ("#line "<> fromString (show (curLine+1))) : goStart (curLine + 1) lns-              | otherwise -> (pre<>pos)-                             : ("#line "<> fromString (show (curLine+1)))-                             : goStart (curLine+1) lns--commentRemoval :: Stringy s => [s] -> [s]-commentRemoval = removeMultilineComments 1 . map dropOneLineBlockComments---- * TOKEN Splices---- | Deal with the two-character '##' token pasting/splicing--- operator. We do so eliminating spaces around the @##@--- operator.-prepTOKENSplices :: [TOKEN] -> [TOKEN]-prepTOKENSplices = map (fmap copy) . dropSpaces [] . mergeTOKENs []-  where -- Merges ## tokens, and reverses the input list-        mergeTOKENs acc [] = acc-        mergeTOKENs acc (Important "#" : Important "#" : ts) =-          mergeTOKENs (Important "##" : acc) (dropWhile (not . isImportant) ts)-        mergeTOKENs acc (t:ts) = mergeTOKENs (t : acc) ts-        -- Drop trailing spaces and re-reverse the list-        dropSpaces acc [] = acc-        dropSpaces acc (t@(Important "##") : ts) =-          dropSpaces (t : acc) (dropWhile (not . isImportant) ts)-        dropSpaces acc (t:ts) = dropSpaces (t : acc) ts---- * Function-like macros as Haskell functions---- | @functionMacro parameters body arguments@ substitutes @arguments@--- for @parameters@ in @body@ and performs stringification for uses of--- the @#@ operator and token concatenation for the @##@ operator.-functionMacro :: [String] -> [TOKEN] -> [([Scan],String)] -> [Scan]-functionMacro params body = paste-                          . subst body'-                          -- . M.fromList-                          . zip params'-  where params' = map copy params-        subst toks gamma = go toks-          where go [] = []-                go (p@(Important "##"):t@(Important s):ts) =-                  case lookup s gamma of-                    Nothing -> Rescan p : Rescan t : go ts-                    Just (_,arg) ->-                      Rescan p : Rescan (Important arg) : go ts-                go (t@(Important s):p@(Important "##"):ts) =-                  case lookup s gamma of-                    Nothing -> Rescan t : go (p:ts)-                    Just (_,arg) -> Rescan (Important arg) : go (p:ts)-                go (t@(Important "##"):ts) = Rescan t : go ts-                go (t@(Important ('#':.s)) : ts) =-                  case lookup s gamma of-                    Nothing -> Rescan t : go ts-                    Just (_,arg) ->-                      Rescan (Important (stringify arg)) : go ts-                go (t@(Important s) : ts) =-                  case lookup s gamma of-                    Nothing -> Rescan t : go ts-                    Just (arg,_) -> arg ++ go ts-                go (t:ts) = Rescan t : go ts-        prepStringify [] = []-        prepStringify (Important "#" : ts) =-          case dropWhile (not . isImportant) ts of-            (Important t : ts') -> Important (cons '#' t) : prepStringify ts'-            _ -> Important "#" : ts-        prepStringify (t:ts) = t : prepStringify ts--        body' = prepStringify . prepTOKENSplices $-                dropWhile (not . isImportant) body-        paste [] = []-        paste (Rescan (Important s) : Rescan (Important "##")-              : Rescan (Important t) : ts) =-          paste (Rescan (Important (trimSpaces s <> sdropWhile isSpace t)) : ts)-        paste (t:ts) = t : paste ts---- * Pre-Processor Capabilities--modifyState :: (Monad m, HasHppState m) => (HppState -> HppState) -> m ()-modifyState f = getState >>= setState . f---- | 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-                            setState (st {hppConfig = cfg', hppLineNum = 1})-                            return (cfg, ln)-     insertInputSegment-       s (modifyState (setL lineNum oldLine . setL config oldCfg))---- * Finding @include@ files--includeCandidates :: [FilePath] -> P.String -> Maybe [FilePath]-includeCandidates searchPath nm =-  case nm of-    '<':nm' -> Just $ sysSearch (init nm')-    '"':nm' -> let nm'' = init nm'-                in Just $ nm'' : sysSearch nm''-    _ -> Nothing-  where sysSearch f = map (</> f) searchPath--searchForInclude :: [FilePath] -> P.String -> IO (Maybe FilePath)-searchForInclude paths = maybe (return Nothing) aux . includeCandidates paths-  where aux [] = return Nothing-        aux (f:fs) = do exists <- doesFileExist f-                        if exists then return (Just f) else aux fs--searchForNextInclude :: [FilePath] -> P.String -> IO (Maybe FilePath)-searchForNextInclude paths = maybe (return Nothing) (aux False)-                           . includeCandidates paths-  where aux _ [] = return Nothing-        aux n (f:fs) = do exists <- doesFileExist f-                          if exists-                          then if n-                               then return (Just f)-                               else aux True fs-                          else aux n fs---- * Running an Hpp Action--runHpp :: forall m a src. (MonadIO m, HasHppState m)-       => (FilePath -> m src)-       -> (src -> m ())-       -> HppT src m a-       -> m (Either (FilePath,Error) a)-runHpp source sink m = runHppT m >>= go-  where go :: FreeF (HppF src) a (HppT src m a)-           -> m (Either (FilePath, Error) a)-        go (PureF x) = return $ Right x-        go (FreeF s) = case s of-          ReadFile ln file k ->-            (includePaths <$> use config)-            >>= liftIO . flip searchForInclude file-            >>= readAux ln file k-          ReadNext ln file k ->-            (includePaths <$> use config)-            >>= liftIO . flip searchForNextInclude file-            >>= readAux ln file k-          WriteOutput output k -> sink output >> runHppT k >>= go--        readAux ln file _ Nothing =-          Left . (, IncludeDoesNotExist ln file) . curFileName <$> use config-        readAux _ln _file k (Just file') =-          source file' >>= runHppT . k >>= go-{-# SPECIALIZE runHpp ::-    (FilePath -> Parser (StateT HppState (ExceptT Error IO)) [TOKEN] [String])- -> ([String] -> Parser (StateT HppState (ExceptT Error IO)) [TOKEN] ())- -> HppT [String] (Parser (StateT HppState (ExceptT Error IO)) [TOKEN]) a- -> Parser (StateT HppState (ExceptT Error IO)) [TOKEN] (Either (FilePath,Error) a) #-}---- * Preprocessor---- | Parse the definition of an object-like or function macro.-parseDefinition :: [TOKEN] -> Maybe (String, Macro)-parseDefinition toks =-  case dropWhile (not . isImportant) toks of-    (Important name:Important "(":rst) ->-      let params = takeWhile (/= ")") $ filter (/= ",") (importants rst)-          body = trimUnimportant . tail $ dropWhile (/= Important ")") toks-          macro = Function (length params) (functionMacro params body)-      in Just (name, macro)-    (Important name:_) ->-      let rhs = case dropWhile (/= Important name) toks of-                  [] -> [Important ""]-                  str@(_:t)-                    | all (not . isImportant) str -> [Important ""]-                    | otherwise -> trimUnimportant t-      in Just (copy name, Object (map (fmap copy) rhs))-    _ -> Nothing---- | 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)---- * State Zooming--expandLineP :: (Monad m, HasHppState m, HasEnv m, HasError m)-            => Parser m [TOKEN] [TOKEN]-expandLineP =-  do st <- getState-     let ln = hppLineNum st-         cfg = hppConfig st-     expandLine cfg ln--hppReadFile :: Monad m => Int -> FilePath -> HppT src m src-hppReadFile n file = HppT (pure (FreeF (ReadFile n file return)))--hppReadNext :: Monad m => Int -> FilePath -> HppT src m src-hppReadNext n file = HppT (pure (FreeF (ReadNext n file return)))---- * Directive Processing---- | Handle preprocessor directives (commands prefixed with an octothorpe).-directive :: forall m. (Monad m, HppCaps 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)-                          Important name <- awaitJust "undef"-                          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 <$> expandLineP)-                       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 (dropBranchLine ln >>= replace . fst)-                     Just _ ->-                       lift (onInputSegment (takeBranchFun 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 (takeBranchFun ln)) -- takeBranch ln >>= precede)-                      Just _ -> lift (dropBranchLine ln >>= replace . fst)-                 _ -> 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 (detokenize (Important t:toks))-          _ -> return False -- Ignore unknown command-        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-                               <$> expandLineP)-             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 expandLineP [squashDefines e toks]))-             let res = evalExpr <$> parseExpr (map (fmap toChars) ex)-             lineNum .= ln-             if maybe False (/= 0) res-               then lift (onInputSegment (takeBranchFun ln)) -- (takeBranch ln >>= precede)-               else lift (dropBranchLine ln >>= replace . fst)--{-# 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--getCmd :: [TOKEN] -> Maybe String-getCmd = aux . dropWhile notImportant-  where aux (Important "#" : ts) = case dropWhile notImportant ts of-                                     (Important cmd:_) -> Just cmd-                                     _ -> Nothing-        aux _ = Nothing--droppingSpaces ::(Monad m) => ParserT m src TOKEN ()-droppingSpaces = droppingWhile notImportant--dropBranchFun :: [[TOKEN]] -> (Int, [[TOKEN]])-dropBranchFun = go (1::Int) 0-  where go _ !n [] = (n,[])-        go !nesting !n (ln:lns) =-          case getCmd ln of-            Just cmd-              | cmd == "endif" -> if nesting == 1-                                  then (n, ln:lns)-                                  else go (nesting-1) (n+1) lns-              | cmd `elem` ["if","ifdef","ifndef"] ->-                go (nesting+1) (n+1) lns-              | cmd `elem` ["else","elif"] -> if nesting == 1-                                              then (n, ln : lns)-                                              else go nesting (n+1) lns-            _ -> go nesting (n+1) lns---- | Take everything up to the end of this branch, drop all remaining--- branches (if any).-takeBranchFun :: LineNum -> [[TOKEN]] -> [[TOKEN]]-takeBranchFun = go (1::Int)-  where go _ _ [] = [] -- error: unterminated conditional-        go 0 !n lns = yieldLineNum n : lns-        go !nesting !n (ln:lns) =-          case getCmd ln of-            Just cmd-              | cmd `elem` ["if","ifdef","ifndef"] ->-                ln : go (nesting+1) (n+1) lns-              | cmd == "endif" -> ln : go (nesting - 1) (n + 1) lns-              | nesting == 1 && cmd `elem` ["else","elif"] ->-                let (numSkipped, lns') = dropBranchFun lns-                in go 1 (n+1+numSkipped) lns'-            _ -> ln : go nesting (n+1) lns---yieldLineNum :: LineNum -> [TOKEN]-yieldLineNum !ln = [Important ("#line " <> fromString (show ln)), Other "\n"]--dropBranchLine :: (HasError m, Monad m)-               => LineNum -> Parser m [TOKEN] ([TOKEN], LineNum)-dropBranchLine !ln = do (el, numSkipped) <- dropBranch-                        let ln' = ln + numSkipped-                        return (yieldLineNum ln' ++ fromMaybe [] el, ln')---- | Skip to the end of a conditional branch. Returns the 'Just' the--- token that ends this branch if it is an @else@ or @elif@, or--- 'Nothing' otherwise, and the number of lines skipped.-dropBranch :: (HasError m, Monad m) => Parser m [TOKEN] (Maybe [TOKEN], Int)-dropBranch = go (1::Int) 0-  where go !nesting !n =-          do ln <- awaitJust "dropBranch"-             case getCmd ln of-               Just cmd-                 | cmd == "endif" -> if nesting == 1-                                     then return (Nothing, n+1)-                                     else go (nesting-1) (n+1)-                 | cmd `elem` ["if","ifdef","ifndef"] ->-                   go (nesting+1) (n+1)-                 | cmd `elem` ["else", "elif"] -> if nesting == 1-                                                  then return (Just ln, n+1)-                                                  else go nesting (n+1)-               _ -> go nesting (n+1)---- | Expands an input line producing a stream of output lines.-macroExpansion :: (Monad m, HppCaps 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 <$> expandLineP)) <* (lineNum %= (+1))---- | The dynamic capabilities offered by HPP-type HppCaps t = (HasError t, HasHppState t, HasEnv t)--parseStreamHpp :: Monad m-               => HppT t (Parser m i) (Maybe t) -> HppT t (Parser m i) ()-parseStreamHpp m = go-  where go = m >>= \case-          Nothing -> return ()-          Just o -> writeOutput o >> go---- * HPP configurations---- | Standard CPP settings for processing C files.-normalCPP :: [String] -> [[TOKEN]]-normalCPP = map ((++ [Other "\n"]) . tokenize)-          . lineSplicing-          -- . map dropLineComments-          . removeMultilineComments 1-          . map (dropOneLineBlockComments . trigraphReplacement)-{-# INLINABLE normalCPP #-}+import qualified Hpp.Config as C+import qualified Hpp.Env as E+import qualified Hpp.RunHpp as R+import qualified Hpp.Types as T+import Hpp.Parser (evalParse, Parser)+import Hpp.StringSig (readLines)+import Hpp.Tokens (tokenize) --- | For Haskell we do not want trigraph replacement.-haskellCPP :: [String] -> [[TOKEN]]-haskellCPP = map ((++[Other "\n"]) . tokenize)-           . lineSplicing-           . commentRemoval-{-# INLINABLE haskellCPP #-}+-- | The type of preprocessor actions. Created with 'preprocess' and+-- executed with 'runHpp' or 'streamHpp'.+newtype HppT m a =+  HppT (T.HppT [ByteString]+               (Parser (StateT T.HppState (ExceptT T.Error m))+                       [T.TOKEN])+               a)+  deriving (Functor, Applicative, Monad) --- | If we don't have a predefined processor, we build one based on a--- 'Config' value.-genericConfig :: Config -> [String] -> [[TOKEN]]-genericConfig cfg = map ((++ [Other "\n"]) . tokenize)-                  . (if spliceLongLines cfg then lineSplicing else id)-                  . (if eraseCComments cfg then commentRemoval else id)-                  . (if replaceTrigraphs cfg then map trigraphReplacement else id)+instance MonadTrans HppT where+  lift = HppT . lift . lift . lift . lift --- * Front End+-- | The result of running hpp+data HppOutput = HppOutput { hppFilesRead :: [FilePath]+                           , hppOutput    :: [ByteString] } -prepareInput :: (Monad m, HppCaps m) => m ([String] -> [[TOKEN]])-prepareInput =-  do cfg <- getL config <$> getState-     case () of-       _ | eraseCComments cfg && spliceLongLines cfg-           && not (inhibitLinemarkers cfg) -> pure normalCPP-       _ | (eraseCComments cfg && spliceLongLines cfg-            && (not (replaceTrigraphs cfg))) ->-           pure haskellCPP-       _ | otherwise -> pure (genericConfig cfg)+-- | Preprocess lines of input.+preprocess :: Monad m => [ByteString] -> HppT m ()+preprocess = HppT . R.preprocess --- | Run a stream of lines through the preprocessor.-preprocess :: (Monad m, HppCaps m)-           => [String] -> HppT [String] (Parser m [TOKEN]) ()-preprocess src =-  do cfg <- getL config <$> getState-     prep <- prepareInput-     let prepOutput = if inhibitLinemarkers cfg then aux else pure-     lift (precede (prep src))-     parseStreamHpp (fmap (prepOutput . detokenize) <$> macroExpansion)-  where aux xs | sIsPrefixOf "#line" xs = []-               | otherwise = [xs]+-- | Run a preprocessor action with some initial state. Returns the+-- result of preprocessing as well as an updated preprocessor state+-- representation.+runHpp :: MonadIO m+       => T.HppState+       -> HppT m a+       -> ExceptT T.Error m (HppOutput, T.HppState)+runHpp st h =+  do r <- liftIO (newIORef id)+     let snk xs = modifyIORef r (. (xs++))+     let fin (x, st') = do outLines <- ($ []) <$> readIORef r+                           return (HppOutput x outLines, st')+     streamHpp st (liftIO . snk) h >>= liftIO . fin --- Note: `preprocess` is the workhorse of the library. We run the--- value it returns in `hppIO'` by interleaving interpretation of--- `HppT` with binds of types providing the `HppCaps`--- capabilities. When making things concrete, we specialize to--- `ExceptT`, `StateT`, and `Parser` (note that `Parser` is actually--- just another `StateT`).+-- | @streamHpp state sink action@ runs a preprocessor @action@ with+-- some initial @state@. Output is streamed to the caller-provided+-- output @sink@ as it is generated. The list of files read during+-- preprocessing is returned along with an updated preprocessor state+-- representation.+streamHpp :: MonadIO m+          => T.HppState+          -> ([ByteString] -> m ())+          -> HppT m a+          -> ExceptT T.Error m ([FilePath], T.HppState)+streamHpp st snk (HppT h) =+  do (a, st') <- runStateT+                   (evalParse+                      (R.runHpp (liftIO . readLines)+                                (lift . lift . lift . snk) h)+                      [])+                   st+     either (throwE . snd) (return . (,st') . R.hppFilesRead) a --- | A concreate choice of types to satisfy `HppCaps` as required by--- `preprocess`.-dischargeHppCaps :: Monad m-                 => Config -> Env-                 -> Parser (StateT HppState (ExceptT Error m))-                           i-                           (Either (a, Error) b)-                 -> m (Maybe Error)-dischargeHppCaps cfg env' m =-  runExceptT-    (evalStateT-       (evalParse (m >>= either (throwError . snd) return) [])-       initialState)-  >>= return . either Just (const Nothing)-  where initialState = setL env env' $ emptyHppState cfg+-- | An 'T.HppState' containing no macro definitions, and default+-- values for the starting configuration: the name of the current file+-- is @\"NoFile\"@, there are no paths to be searched for included+-- files, etc. See 'C.Config' for more information on available+-- configuration.+emptyHppState :: T.HppState+emptyHppState = T.emptyHppState+              $ fromMaybe (error "emptyHppState assumption wrong")+                          (C.realizeConfig cfg)+  where cfg = C.defaultConfigF { C.curFileNameF = Just "NoFile" } --- | General hpp runner against input source file lines; can return an--- 'Error' value if something goes wrong.-hppIO' :: Config -> Env -> ([String] -> IO ()) -> [String] -> IO (Maybe Error)-hppIO' cfg env' snk src =-  dischargeHppCaps cfg env' $-  runHpp (liftIO . readLines) (liftIO . snk) (preprocess src)+-- | Create a 'T.HppState' with the given 'C.Config' and 'T.Env'.+initHppState :: C.Config -> T.Env -> T.HppState+initHppState c e = T.HppState c 1 e --- | General hpp runner against input source file lines. Any errors--- encountered are thrown with 'error'.-hppIO :: Config -> Env -> ([String] -> IO ()) -> [String] -> IO ()-hppIO cfg env' snk = fmap (maybe () (error . show)) . hppIO' cfg env' snk+-- | @addDefinition name expression@ adds a binding of @name@ to+-- @expression@ in the preprocessor’s internal state.+addDefinition :: ByteString -> ByteString -> T.HppState -> Maybe T.HppState+addDefinition name val s = flip (T.over T.env) s . E.insertPair+                           <$> parseDefinition name val --- | hpp runner that returns output lines.-hppFileContents :: Config -> Env ->  FilePath -> [String] -> IO (Either Error [String])-hppFileContents cfg env' fileName src = do-  r <- newIORef id-  let snk xs = modifyIORef r (. (xs++))-  hppIO' (cfg {curFileNameF = pure fileName}) env' snk src >>= \case-    Nothing -> Right . ($ []) <$> readIORef r-    Just e -> return (Left e)+-- | Lower level parsing of macro definitions. Will typically be used+-- with 'E.insertPair' for manual construction of a 'T.Env' binding+-- environment.+parseDefinition :: ByteString -> ByteString -> Maybe (ByteString, T.Macro)+parseDefinition name val = R.parseDefinition (tokenize name ++ tokenize val)
src/Hpp/CmdLine.hs view
@@ -3,12 +3,12 @@ -- line invocation. module Hpp.CmdLine (runWithArgs) where import Control.Monad (unless)+import Control.Monad.Trans.Except (runExceptT) import Data.String (fromString) import Hpp import Hpp.Config import Hpp.Env (deleteKey, emptyEnv, insertPair) import Hpp.StringSig (readLines, putStringy)-import Hpp.Tokens import Hpp.Types (Env, Error(..)) import System.Directory (doesFileExist, makeAbsolute) import System.IO (openFile, IOMode(..), hClose, stdout)@@ -40,11 +40,11 @@             Just cfg' -> return (Right (env, acc [], cfg', out))             Nothing -> return (Left NoInputFile)         go env acc cfg out ("-D":name:"=":body:rst) =-          case parseDefinition (Important (fromString name) : Other " " : tokenize (fromString body)) of+          case parseDefinition (fromString name) (fromString body) of             Nothing -> return . Left $ BadMacroDefinition 0             Just def -> go (insertPair def env) acc cfg out rst         go env acc cfg out ("-D":name:rst) =-          case parseDefinition ([Important (fromString name), Other " ", Important "1"]) of+          case parseDefinition (fromString name) "1" of             Nothing -> return . Left $ BadMacroDefinition 0             Just def -> go (insertPair def env) acc cfg out rst         go env acc cfg out ("-U":name:rst) =@@ -105,11 +105,18 @@                           Just f ->                             do h <- makeAbsolute f >>=                                     flip openFile WriteMode-                               return (\os -> mapM_ (putStringy h) os-                                      ,hClose h)-     _ <- (readLines fileName)-           >>= hppIO cfg' env snk . (map fromString lns ++)-     -- lns' <- (lines <$> readFile fileName)-     --          >>= hppFileContents cfg env fileName . (lns ++)-     -- snk lns'+                               return ( \os -> mapM_ (putStringy h) os+                                      , hClose h )+     let errorExcept = fmap (either (error . show) id) . runExceptT+     _ <- readLines fileName+           >>= errorExcept+               . streamHpp (initHppState cfg' env) snk+               . preprocess+               . (map fromString lns ++)+     -- (lns', _) <- readLines fileName+     --              >>= errorExcept+     --                  . runHpp (initHppState cfg' env)+     --                  . preprocess+     --                  . (map fromString lns ++)+     -- snk (hppOutput lns')      closeSnk
src/Hpp/Config.hs view
@@ -18,13 +18,32 @@ -- Identity@. Specifically, the source file name of the file being -- processed /must/ be set. data ConfigF f = Config { curFileNameF        :: f FilePath+                          -- ^ Name of the file being+                          -- preprocessed. Hpp will update this as new+                          -- files are included. The user must set it+                          -- manually for the starting input file.                         , includePathsF       :: f [FilePath]+                        -- ^ Paths to be searched for included files.                         , spliceLongLinesF    :: f Bool+                        -- ^ A backslash as the last character of a+                        -- line causes the next line to be appended to+                        -- the current one eliding the newline+                        -- character present in the source input.                         , eraseCCommentsF     :: f Bool+                        -- ^ Erase line comments (starting with @//@)+                        -- and block comments (delimited by @/*@ and+                        -- @*/@).                         , inhibitLinemarkersF :: f Bool+                        -- ^ Do not emit @#line@ directives.                         , replaceTrigraphsF   :: f Bool+                        -- ^ Replace trigraph sequences (each of which+                        -- starts with two consecutive question marks+                        -- (@\"??\"@) with the characters they encode.                         , prepDateF           :: f DateString-                        , prepTimeF           :: f TimeString }+                        -- ^ Format string for @\_\_DATE\_\_@.+                        , prepTimeF           :: f TimeString+                        -- ^ Format string for @\_\_TIME\_\_@.+                       }  -- | A fully-populated configuration for the pre-processor. type Config = ConfigF Identity@@ -75,7 +94,9 @@ prepTime :: Config -> TimeString prepTime = runIdentity . prepTimeF --- | A default configuration with no current file name set.+-- | A default configuration with no current file name set. Note that+-- long line splicing is enabled, C++-style comments are erased, #line+-- markers are inhibited, and trigraph replacement is disabled. defaultConfigF :: ConfigF Maybe defaultConfigF = Config Nothing (Just [])                         (Just True) (Just True) (Just True) (Just False)
+ src/Hpp/RunHpp.hs view
@@ -0,0 +1,659 @@+{-# LANGUAGE BangPatterns, ConstraintKinds, LambdaCase, OverloadedStrings,+             ScopedTypeVariables, TupleSections, ViewPatterns #-}+-- | Mid-level interface to the pre-processor.+module Hpp.RunHpp (parseDefinition, preprocess, runHpp,+                   hppIOSink, HppCaps, hppIO, HppResult(..)) where+import Control.Arrow (first)+import Control.Exception (throwIO)+import Control.Monad (unless, (>=>))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Except+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.State.Strict (StateT, evalStateT)+import Data.Char (isSpace)+import Data.IORef+import Data.Maybe (fromMaybe)+import Data.Monoid ((<>))+import Data.String (fromString)+import Hpp.Config (Config, curFileNameF, curFileName, includePaths,+                   eraseCComments, spliceLongLines, inhibitLinemarkers,+                   replaceTrigraphs)+import Hpp.Env (deleteKey, insertPair, lookupKey)+import Hpp.Expansion (expandLine)+import Hpp.Expr (evalExpr, parseExpr)+import Hpp.Parser (Parser, ParserT, replace, await, awaitJust, droppingWhile,+                   precede, takingWhile, insertInputSegment, onElements,+                   evalParse, onInputSegment)+import Hpp.StringSig+import Hpp.Tokens (Token(..), importants, isImportant, newLine, trimUnimportant,+                   detokenize, notImportant, tokenize, skipLiteral)+import Hpp.Types+import System.Directory (doesFileExist)+import System.FilePath ((</>))+import Text.Read (readMaybe)+import Prelude hiding (String)+import qualified Prelude as P++-- * Trigraphs++-- | The first component of each pair represents the end of a known+-- trigraph sequence (each trigraph begins with two consecutive+-- question marks (@\"??\"@). The second component is the+-- single-character equivalent that we substitute in for the trigraph.+trigraphs :: [(Char, Char)]+trigraphs = [ ('=', '#')+            , ('/', '\\')+            , ('\'', '^')+            , ('(', '[')+            , (')', ']')+            , ('!', '|')+            , ('<', '{')+            , ('>', '}')+            , ('-', '~') ]++trigraphReplacement :: Stringy s => s -> s+trigraphReplacement s = aux (breakOn [("??", ())] s)+  where aux Nothing = s+        aux (Just (_, pre, pos)) =+          case uncons pos of+            Nothing -> pre <> "??"+            Just (c,t) ->+              case lookup c trigraphs of+                Just c' -> snoc pre c' <> trigraphReplacement t+                Nothing -> snoc pre '?' <> trigraphReplacement (cons '?' pos)++-- * Line Splicing++-- | If a line ends with a backslash, it is prepended to the following+-- the line.+lineSplicing :: Stringy s => [s] -> [s]+lineSplicing = go id+  where go acc [] = [acc mempty]+        go acc (ln:lns) = case unsnoc ln of+                            Nothing -> acc mempty : go id lns+                            Just (ini, '\\') -> go (acc . (ini<>)) lns+                            Just _ -> acc ln : go id lns+{-# INLINE lineSplicing #-}++-- * C Comments++breakBlockCommentStart :: Stringy s => s -> Maybe (s, s)+breakBlockCommentStart s =+  case breakCharOrSub '"' "/*" s of+    NoMatch -> Nothing+    CharMatch pre pos -> let (lit, rest) = skipLiteral pos+                         in first ((pre <> lit) <>) <$>+                            breakBlockCommentStart rest+    SubMatch pre pos -> Just (pre, pos)++breakBlockCommentEnd :: Stringy s => s -> Maybe s+breakBlockCommentEnd s =+  case breakCharOrSub '"' "*/" s of+    NoMatch -> Nothing+    CharMatch _ pos -> let (_, rest) = skipLiteral pos+                       in breakBlockCommentEnd rest+    SubMatch _ pos -> Just pos++dropOneLineBlockComments :: Stringy s => s -> s+dropOneLineBlockComments s =+  case breakCharOrSub '"' "/*"s of+    NoMatch -> s+    CharMatch pre pos ->+      let (lit,rest) = skipLiteral pos+      in snoc pre '"' <> lit <> dropOneLineBlockComments rest+    SubMatch pre pos ->+      case breakOn [("*/", ())] pos of+        Nothing -> pre <> "/*"+        Just (_,_,pos') -> snoc pre ' ' <> dropOneLineBlockComments pos'++removeMultilineComments :: Stringy s => Int -> [s] -> [s]+removeMultilineComments !lineStart = goStart lineStart+  where goStart _ [] = []+        goStart !curLine (ln:lns) =+          case breakBlockCommentStart ln of+            Nothing -> ln : goStart (curLine+1) lns+            Just (pre,_) -> goEnd (curLine+1) pre lns+        goEnd _ _ [] = error "Unmatched /*"+        goEnd !curLine pre (ln:lns) =+          case breakBlockCommentEnd ln of+            Nothing -> goEnd (curLine+1) pre lns+            Just pos+              | sall isSpace (pre<>pos) ->+                ("#line "<> fromString (show (curLine+1))) : goStart (curLine + 1) lns+              | otherwise -> (pre<>pos)+                             : ("#line "<> fromString (show (curLine+1)))+                             : goStart (curLine+1) lns++commentRemoval :: Stringy s => [s] -> [s]+commentRemoval = removeMultilineComments 1 . map dropOneLineBlockComments++-- * TOKEN Splices++-- | Deal with the two-character '##' token pasting/splicing+-- operator. We do so eliminating spaces around the @##@+-- operator.+prepTOKENSplices :: [TOKEN] -> [TOKEN]+prepTOKENSplices = map (fmap copy) . dropSpaces [] . mergeTOKENs []+  where -- Merges ## tokens, and reverses the input list+        mergeTOKENs acc [] = acc+        mergeTOKENs acc (Important "#" : Important "#" : ts) =+          mergeTOKENs (Important "##" : acc) (dropWhile (not . isImportant) ts)+        mergeTOKENs acc (t:ts) = mergeTOKENs (t : acc) ts+        -- Drop trailing spaces and re-reverse the list+        dropSpaces acc [] = acc+        dropSpaces acc (t@(Important "##") : ts) =+          dropSpaces (t : acc) (dropWhile (not . isImportant) ts)+        dropSpaces acc (t:ts) = dropSpaces (t : acc) ts++-- * Function-like macros as Haskell functions++-- | @functionMacro parameters body arguments@ substitutes @arguments@+-- for @parameters@ in @body@ and performs stringification for uses of+-- the @#@ operator and token concatenation for the @##@ operator.+functionMacro :: [String] -> [TOKEN] -> [([Scan],String)] -> [Scan]+functionMacro params body = paste+                          . subst body'+                          -- . M.fromList+                          . zip params'+  where params' = map copy params+        subst toks gamma = go toks+          where go [] = []+                go (p@(Important "##"):t@(Important s):ts) =+                  case lookup s gamma of+                    Nothing -> Rescan p : Rescan t : go ts+                    Just (_,arg) ->+                      Rescan p : Rescan (Important arg) : go ts+                go (t@(Important s):p@(Important "##"):ts) =+                  case lookup s gamma of+                    Nothing -> Rescan t : go (p:ts)+                    Just (_,arg) -> Rescan (Important arg) : go (p:ts)+                go (t@(Important "##"):ts) = Rescan t : go ts+                go (t@(Important ('#':.s)) : ts) =+                  case lookup s gamma of+                    Nothing -> Rescan t : go ts+                    Just (_,arg) ->+                      Rescan (Important (stringify arg)) : go ts+                go (t@(Important s) : ts) =+                  case lookup s gamma of+                    Nothing -> Rescan t : go ts+                    Just (arg,_) -> arg ++ go ts+                go (t:ts) = Rescan t : go ts+        prepStringify [] = []+        prepStringify (Important "#" : ts) =+          case dropWhile (not . isImportant) ts of+            (Important t : ts') -> Important (cons '#' t) : prepStringify ts'+            _ -> Important "#" : ts+        prepStringify (t:ts) = t : prepStringify ts++        body' = prepStringify . prepTOKENSplices $+                dropWhile (not . isImportant) body+        paste [] = []+        paste (Rescan (Important s) : Rescan (Important "##")+              : Rescan (Important t) : ts) =+          paste (Rescan (Important (trimSpaces s <> sdropWhile isSpace t)) : ts)+        paste (t:ts) = t : paste ts++-- * Pre-Processor Capabilities++modifyState :: (Monad m, HasHppState m) => (HppState -> HppState) -> m ()+modifyState f = getState >>= setState . f++-- | 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+                            setState (st {hppConfig = cfg', hppLineNum = 1})+                            return (cfg, ln)+     insertInputSegment+       s (modifyState (setL lineNum oldLine . setL config oldCfg))++-- * Finding @include@ files++includeCandidates :: [FilePath] -> P.String -> Maybe [FilePath]+includeCandidates searchPath nm =+  case nm of+    '<':nm' -> Just $ sysSearch (init nm')+    '"':nm' -> let nm'' = init nm'+                in Just $ nm'' : sysSearch nm''+    _ -> Nothing+  where sysSearch f = map (</> f) searchPath++searchForInclude :: [FilePath] -> P.String -> IO (Maybe FilePath)+searchForInclude paths = maybe (return Nothing) aux . includeCandidates paths+  where aux [] = return Nothing+        aux (f:fs) = do exists <- doesFileExist f+                        if exists then return (Just f) else aux fs++searchForNextInclude :: [FilePath] -> P.String -> IO (Maybe FilePath)+searchForNextInclude paths = maybe (return Nothing) (aux False)+                           . includeCandidates paths+  where aux _ [] = return Nothing+        aux n (f:fs) = do exists <- doesFileExist f+                          if exists+                          then if n+                               then return (Just f)+                               else aux True fs+                          else aux n fs++-- * Running an Hpp Action++data HppResult a = HppResult { hppFilesRead :: [FilePath]+                             , hppResult :: a }++runHpp :: forall m a src. (MonadIO m, HasHppState m)+       => (FilePath -> m src)+       -> (src -> m ())+       -> HppT src m a+       -> m (Either (FilePath,Error) (HppResult a))+runHpp source sink m = runHppT m >>= go []+  where go :: [FilePath]+           -> FreeF (HppF src) a (HppT src m a)+           -> m (Either (FilePath, Error) (HppResult a))+        go files (PureF x) = return $ Right (HppResult files x)+        go files (FreeF s) = case s of+          ReadFile ln file k ->+            (includePaths <$> use config)+            >>= liftIO . flip searchForInclude file+            >>= readAux (file:files) ln file k+          ReadNext ln file k ->+            (includePaths <$> use config)+            >>= liftIO . flip searchForNextInclude file+            >>= readAux (file:files) ln file k+          WriteOutput output k -> sink output >> runHppT k >>= go files++        readAux _files ln file _ Nothing =+          Left . (, IncludeDoesNotExist ln file) . curFileName <$> use config+        readAux files _ln _file k (Just file') =+          source file' >>= runHppT . k >>= go files+{-# SPECIALIZE runHpp ::+    (FilePath -> Parser (StateT HppState (ExceptT Error IO)) [TOKEN] [String])+ -> ([String] -> Parser (StateT HppState (ExceptT Error IO)) [TOKEN] ())+ -> HppT [String] (Parser (StateT HppState (ExceptT Error IO)) [TOKEN]) a+ -> Parser (StateT HppState (ExceptT Error IO)) [TOKEN] (Either (FilePath,Error) (HppResult a)) #-}++-- * Preprocessor++-- | Parse the definition of an object-like or function macro.+parseDefinition :: [TOKEN] -> Maybe (String, Macro)+parseDefinition toks =+  case dropWhile (not . isImportant) toks of+    (Important name:Important "(":rst) ->+      let params = takeWhile (/= ")") $ filter (/= ",") (importants rst)+          body = trimUnimportant . tail $ dropWhile (/= Important ")") toks+          macro = Function (length params) (functionMacro params body)+      in Just (name, macro)+    (Important name:_) ->+      let rhs = case dropWhile (/= Important name) toks of+                  [] -> [Important ""]+                  str@(_:t)+                    | all (not . isImportant) str -> [Important ""]+                    | otherwise -> trimUnimportant t+      in Just (copy name, Object (map (fmap copy) rhs))+    _ -> Nothing++-- | 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)++-- * State Zooming++expandLineP :: (Monad m, HasHppState m, HasEnv m, HasError m)+            => Parser m [TOKEN] [TOKEN]+expandLineP =+  do st <- getState+     let ln = hppLineNum st+         cfg = hppConfig st+     expandLine cfg ln++-- | @hppReadFile lineNumber fileName@ introduces an @#include+-- <fileName>@ as if it occurred at the given line number.+hppReadFile :: Monad m => Int -> FilePath -> HppT src m src+hppReadFile n file = HppT (pure (FreeF (ReadFile n file return)))++-- | @hppReadNext lineNumber fileName@ introduces an @#include_next+-- <fileName>@ as if it occurred at the given line number.+hppReadNext :: Monad m => Int -> FilePath -> HppT src m src+hppReadNext n file = HppT (pure (FreeF (ReadNext n file return)))++-- * Directive Processing++-- | Handle preprocessor directives (commands prefixed with an octothorpe).+directive :: forall m. (Monad m, HppCaps 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)+                          Important name <- awaitJust "undef"+                          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 <$> expandLineP)+                       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 (dropBranchLine ln >>= replace . fst)+                     Just _ ->+                       lift (onInputSegment (takeBranchFun 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 (takeBranchFun ln)) -- takeBranch ln >>= precede)+                      Just _ -> lift (dropBranchLine ln >>= replace . fst)+                 _ -> 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 (detokenize (Important t:toks))+          _ -> return False -- Ignore unknown command+        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+                               <$> expandLineP)+             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 expandLineP [squashDefines e toks]))+             let res = evalExpr <$> parseExpr (map (fmap toChars) ex)+             lineNum .= ln+             if maybe False (/= 0) res+               then lift (onInputSegment (takeBranchFun ln)) -- (takeBranch ln >>= precede)+               else lift (dropBranchLine ln >>= replace . fst)++{-# 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++getCmd :: [TOKEN] -> Maybe String+getCmd = aux . dropWhile notImportant+  where aux (Important "#" : ts) = case dropWhile notImportant ts of+                                     (Important cmd:_) -> Just cmd+                                     _ -> Nothing+        aux _ = Nothing++droppingSpaces ::(Monad m) => ParserT m src TOKEN ()+droppingSpaces = droppingWhile notImportant++dropBranchFun :: [[TOKEN]] -> (Int, [[TOKEN]])+dropBranchFun = go (1::Int) 0+  where go _ !n [] = (n,[])+        go !nesting !n (ln:lns) =+          case getCmd ln of+            Just cmd+              | cmd == "endif" -> if nesting == 1+                                  then (n, ln:lns)+                                  else go (nesting-1) (n+1) lns+              | cmd `elem` ["if","ifdef","ifndef"] ->+                go (nesting+1) (n+1) lns+              | cmd `elem` ["else","elif"] -> if nesting == 1+                                              then (n, ln : lns)+                                              else go nesting (n+1) lns+            _ -> go nesting (n+1) lns++-- | Take everything up to the end of this branch, drop all remaining+-- branches (if any).+takeBranchFun :: LineNum -> [[TOKEN]] -> [[TOKEN]]+takeBranchFun = go (1::Int)+  where go _ _ [] = [] -- error: unterminated conditional+        go 0 !n lns = yieldLineNum n : lns+        go !nesting !n (ln:lns) =+          case getCmd ln of+            Just cmd+              | cmd `elem` ["if","ifdef","ifndef"] ->+                ln : go (nesting+1) (n+1) lns+              | cmd == "endif" -> ln : go (nesting - 1) (n + 1) lns+              | nesting == 1 && cmd `elem` ["else","elif"] ->+                let (numSkipped, lns') = dropBranchFun lns+                in go 1 (n+1+numSkipped) lns'+            _ -> ln : go nesting (n+1) lns+++yieldLineNum :: LineNum -> [TOKEN]+yieldLineNum !ln = [Important ("#line " <> fromString (show ln)), Other "\n"]++dropBranchLine :: (HasError m, Monad m)+               => LineNum -> Parser m [TOKEN] ([TOKEN], LineNum)+dropBranchLine !ln = do (el, numSkipped) <- dropBranch+                        let ln' = ln + numSkipped+                        return (yieldLineNum ln' ++ fromMaybe [] el, ln')++-- | Skip to the end of a conditional branch. Returns the 'Just' the+-- token that ends this branch if it is an @else@ or @elif@, or+-- 'Nothing' otherwise, and the number of lines skipped.+dropBranch :: (HasError m, Monad m) => Parser m [TOKEN] (Maybe [TOKEN], Int)+dropBranch = go (1::Int) 0+  where go !nesting !n =+          do ln <- awaitJust "dropBranch"+             case getCmd ln of+               Just cmd+                 | cmd == "endif" -> if nesting == 1+                                     then return (Nothing, n+1)+                                     else go (nesting-1) (n+1)+                 | cmd `elem` ["if","ifdef","ifndef"] ->+                   go (nesting+1) (n+1)+                 | cmd `elem` ["else", "elif"] -> if nesting == 1+                                                  then return (Just ln, n+1)+                                                  else go nesting (n+1)+               _ -> go nesting (n+1)++-- | Expands an input line producing a stream of output lines.+macroExpansion :: (Monad m, HppCaps 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 <$> expandLineP)) <* (lineNum %= (+1))++-- | The dynamic capabilities offered by HPP+type HppCaps t = (HasError t, HasHppState t, HasEnv t)++parseStreamHpp :: Monad m+               => HppT t (Parser m i) (Maybe t) -> HppT t (Parser m i) ()+parseStreamHpp m = go+  where go = m >>= \case+          Nothing -> return ()+          Just o -> writeOutput o >> go++-- * HPP configurations++-- | Standard CPP settings for processing C files.+normalCPP :: [String] -> [[TOKEN]]+normalCPP = map ((++ [Other "\n"]) . tokenize)+          . lineSplicing+          -- . map dropLineComments+          . removeMultilineComments 1+          . map (dropOneLineBlockComments . trigraphReplacement)+{-# INLINABLE normalCPP #-}++-- | For Haskell we do not want trigraph replacement.+haskellCPP :: [String] -> [[TOKEN]]+haskellCPP = map ((++[Other "\n"]) . tokenize)+           . lineSplicing+           . commentRemoval+{-# INLINABLE haskellCPP #-}++-- | If we don't have a predefined processor, we build one based on a+-- 'Config' value.+genericConfig :: Config -> [String] -> [[TOKEN]]+genericConfig cfg = map ((++ [Other "\n"]) . tokenize)+                  . (if spliceLongLines cfg then lineSplicing else id)+                  . (if eraseCComments cfg then commentRemoval else id)+                  . (if replaceTrigraphs cfg then map trigraphReplacement else id)++-- * Front End++prepareInput :: (Monad m, HppCaps m) => m ([String] -> [[TOKEN]])+prepareInput =+  do cfg <- getL config <$> getState+     case () of+       _ | eraseCComments cfg && spliceLongLines cfg+           && not (inhibitLinemarkers cfg) -> pure normalCPP+       _ | (eraseCComments cfg && spliceLongLines cfg+            && (not (replaceTrigraphs cfg))) ->+           pure haskellCPP+       _ | otherwise -> pure (genericConfig cfg)++-- | Run a stream of lines through the preprocessor.+preprocess :: (Monad m, HppCaps m)+           => [String] -> HppT [String] (Parser m [TOKEN]) ()+preprocess src =+  do cfg <- getL config <$> getState+     prep <- prepareInput+     let prepOutput = if inhibitLinemarkers cfg then aux else pure+     lift (precede (prep src))+     parseStreamHpp (fmap (prepOutput . detokenize) <$> macroExpansion)+  where aux xs | sIsPrefixOf "#line" xs = []+               | otherwise = [xs]++-- Note: `preprocess` is the workhorse of the library. We run the+-- value it returns in `hppIO'` by interleaving interpretation of+-- `HppT` with binds of types providing the `HppCaps`+-- capabilities. When making things concrete, we specialize to+-- `ExceptT`, `StateT`, and `Parser` (note that `Parser` is actually+-- just another `StateT`).++-- | A concreate choice of types to satisfy `HppCaps` as required by+-- `preprocess`.+dischargeHppCaps :: Monad m+                 => Config -> Env+                 -> Parser (StateT HppState (ExceptT Error m))+                           i+                           (Either (a, Error) b)+                 -> m (Either Error b)+dischargeHppCaps cfg env' m =+  runExceptT+    (evalStateT+       (evalParse (m >>= either (throwError . snd) return) [])+       initialState)+  where initialState = setL env env' $ emptyHppState cfg++-- | General hpp runner against input source file lines; can return an+-- 'Error' value if something goes wrong.+hppIOSink' :: Config -> Env -> ([String] -> IO ()) -> [String]+           -> IO (Either Error [FilePath])+hppIOSink' cfg env' snk src =+  fmap (fmap hppFilesRead)+  . dischargeHppCaps cfg env' $+  runHpp (liftIO . readLines) (liftIO . snk) (preprocess src)++-- | General hpp runner against input source file lines. Output lines+-- are fed to the caller-supplied sink function. Any errors+-- encountered are thrown with 'error'.+hppIOSink :: Config -> Env -> ([String] -> IO ()) -> [String] -> IO [FilePath]+hppIOSink cfg env' snk = hppIOSink' cfg env' snk >=> either throwIO return++-- | hpp runner that returns output lines.+hppIO :: Config -> Env ->  FilePath -> [String]+      -> IO (Either Error ([FilePath], [String]))+hppIO cfg env' fileName src = do+  r <- newIORef id+  let snk xs = modifyIORef r (. (xs++))+  hppIOSink' (cfg {curFileNameF = pure fileName}) env' snk src >>= \case+    Left e -> return (Left e)+    Right files -> Right . (files,) . ($ []) <$> readIORef r
src/Hpp/Tokens.hs view
@@ -72,6 +72,7 @@                   , ('\'', 0x27), ('"', 0x22), ('?', 0x3F) ]  data TokChar = TokSpace Char | TokQuote | TokDQuote+ -- | Break a 'String' into space and non-whitespace runs. tokWords :: Stringy s => s -> [Token s] tokWords s =
src/Hpp/Types.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE FlexibleInstances, LambdaCase, Rank2Types #-} -- | The core types involved used by the pre-processor. module Hpp.Types where+import Control.Exception (Exception (..)) import Control.Monad (ap) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Trans.Class (MonadTrans, lift)@@ -22,8 +23,6 @@ type LineNum = Int  -- | A macro binding environment.--- type Env = [(String, Macro)]--- type Env = M.Map String Macro type Env = T.Trie Macro  -- * Changing the underlying string type@@ -47,6 +46,8 @@            | BadCommandLine P.String            | RanOutOfInput              deriving (Eq, Ord, Show)++instance Exception Error  -- | Hpp can raise various parsing errors. class HasError m where
+ tests/AsLib.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE LambdaCase, OverloadedStrings #-}+import Control.Monad.Trans.Except+import Data.ByteString.Char8 (ByteString)+import Data.Maybe (fromMaybe)+import Data.Monoid ((<>))+import Hpp+import System.Exit++sourceIfdef :: [ByteString]+sourceIfdef = [ "#ifdef FOO"+              , "x = 42"+              , "#else"+              , "x = 99"+              , "#endif" ]++sourceArith1 :: ByteString -> [ByteString]+sourceArith1 s = [ "#define x 3"+                 , "#if 5 + x > " <> s+                 , "yay"+                 , "#else"+                 , "boo"+                 , "#endif" ]++hppHelper :: HppState -> [ByteString] -> [ByteString] -> IO Bool+hppHelper st src expected = runExceptT (runHpp st (preprocess src)) >>= \case+  Left e -> putStrLn ("Error running hpp: " ++ show e) >> return False+  Right (res, _) -> if hppOutput res == expected+                       then return True+                       else do putStr ("Expected "++show expected++", got")+                               print (hppOutput res)+                               return False++testElse :: IO Bool+testElse = hppHelper emptyHppState sourceIfdef ["x = 99\n","\n"]++testIf :: IO Bool+testIf = hppHelper (fromMaybe (error "Preprocessor definition did not parse")+                              (addDefinition "FOO" "1" emptyHppState))+                   sourceIfdef+                   ["x = 42\n","\n"]++testArith1 :: IO Bool+testArith1 = (&&) <$> hppHelper emptyHppState (sourceArith1 "7") ["yay\n","\n"]+                  <*> hppHelper emptyHppState (sourceArith1 "8") ["boo\n","\n"]++main :: IO ()+main = do results <- sequenceA [testElse, testIf, testArith1]+          if and results then exitWith ExitSuccess else exitWith (ExitFailure 1)