hpc-codecov-0.4.0.0: src/Trace/Hpc/Codecov/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
-- |
-- Module: Trace.Hpc.Codecov.Parser
-- Copyright: (c) 2023 8c6794b6
-- License: BSD3
-- Maintainer: 8c6794b6 <8c6794b6@gmail.com>
--
-- Simple bytestring parser for reading @.mix@ and @.tix@ files.
--
-- @since 0.4.0.0
module Trace.Hpc.Codecov.Parser
( readTix'
, readMix'
) where
-- base
import Control.Applicative (Alternative (..))
import Data.Functor (($>))
import Prelude hiding (takeWhile)
-- bytestring
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as BS
-- filepath
import System.FilePath ((<.>), (</>))
-- hpc
import Trace.Hpc.Mix (BoxLabel (..),
CondBox (..), Mix (..),
MixEntry)
import Trace.Hpc.Tix (Tix (..), TixModule (..),
tixModuleName)
import Trace.Hpc.Util (HpcHash (..), HpcPos,
catchIO, toHpcPos)
-- time
import Data.Time.Calendar.OrdinalDate (fromOrdinalDate)
import Data.Time.Clock (UTCTime (..))
-- ------------------------------------------------------------------------
-- Exported
-- ------------------------------------------------------------------------
-- | Read a @.tix@ File.
--
-- This function is similar to 'Trace.Hpc.Tix.readTix', but internally
-- uses 'ByteString' to improve performance.
readTix' :: FilePath -> IO (Maybe Tix)
readTix' path =
(runMaybeP parseTix <$> BS.readFile path) `catchIO` const (pure Nothing)
-- | Read a @.mix@ file.
--
-- This function is similar to 'Trace.Hpc.Mix.readMix', but internally
-- uses 'ByteString' to improve performance.
--
-- __NOTE__: At the moment, the 'UTCTime' field in the parsed 'Mix' is
-- constantly filled with dummy value, to avoid parsing date time.
readMix'
:: [String] -- ^ Dir names
-> Either String TixModule -- ^ module wanted
-> IO Mix
readMix' dirs et_tm = go dirs
where
mixname = either id tixModuleName et_tm <.> "mix"
handler _ = pure (Left "err ...")
parse path = runEitherP parseMix <$> BS.readFile path
go [] = error "Cannot find mix file"
go (d:ds) = do
et_mix <- parse (d </> mixname) `catchIO` handler
case et_mix of
Right mix -> pure mix
Left _err -> go ds
-- ------------------------------------------------------------------------
-- The parser
-- ------------------------------------------------------------------------
newtype P a =
P {runP :: forall r. (String -> r) -- On error
-> (a -> ByteString -> r) -- On success
-> ByteString -- Input to consume
-> r}
instance Functor P where
fmap f p = P (\err ok -> runP p err (ok . f))
{-# INLINE fmap #-}
instance Applicative P where
pure x = P (\_ ok -> ok x)
{-# INLINE pure #-}
pf <*> pa = P (\err ok -> runP pf err (\f -> runP (fmap f pa) err ok))
{-# INLINE (<*>) #-}
instance Monad P where
m >>= k = P (\err ok -> runP m err (\x -> runP (k x) err ok))
{-# INLINE (>>=) #-}
instance Alternative P where
empty = P (\err _ _ -> err "Alternative.empty")
{-# INLINE empty #-}
p1 <|> p2 = P (\err go bs -> runP p1 (\_ -> runP p2 err go bs) go bs)
{-# INLINE (<|>) #-}
runEitherP :: P a -> ByteString -> Either String a
runEitherP p = runP p Left (\a _ -> Right a)
runMaybeP :: P a -> ByteString -> Maybe a
runMaybeP p = runP p (const Nothing) (\a _ -> Just a)
char :: Char -> P ()
char c =
P (\err ok bs ->
case BS.uncons bs of
Just (c', bs') | c == c' -> ok () bs'
_ -> err ("char: failed to get " <> show c))
{-# INLINABLE char #-}
bytes :: ByteString -> P ()
bytes target =
P (\err ok bs ->
case BS.splitAt (BS.length target) bs of
(pre, post) | pre == target -> ok () post
_ -> err ("bytes: failed to parse `" <> show target <> "'"))
{-# INLINABLE bytes #-}
int :: P Int
int =
P (\err ok bs ->
case BS.readInt bs of
Just (n, bs') -> ok n bs'
_ -> err "int: failed")
{-# INLINABLE int #-}
integer :: P Integer
integer = fmap fromIntegral int
{-# INLINEABLE integer #-}
spaces :: P ()
spaces = P (\_ ok bs -> ok () (BS.dropSpace bs))
{-# INLINABLE spaces #-}
takeWhile :: (Char -> Bool) -> P ByteString
takeWhile test =
P (\_ ok bs -> case BS.span test bs of (pre, post) -> ok pre post)
{-# INLINABLE takeWhile #-}
sepBy :: Alternative f => f a -> f s -> f [a]
sepBy a s = sepBy1 a s <|> pure []
{-# INLINEABLE sepBy #-}
sepBy1 :: Alternative f => f a -> f s -> f [a]
sepBy1 a s = go
where
go = (:) <$> a <*> ((s *> go) <|> pure [])
{-# INLINABLE sepBy1 #-}
doubleQuoted :: P a -> P a
doubleQuoted p = char '"' *> p <* char '"'
{-# INLINEABLE doubleQuoted #-}
bracketed :: P a -> P a
bracketed p = char '[' *> p <* char ']'
{-# INLINABLE bracketed #-}
parenthesized :: P a -> P a
parenthesized p = char '(' *> p <* char ')'
{-# INLINABLE parenthesized #-}
comma :: P ()
comma = char ','
{-# INLINABLE comma #-}
bool :: P Bool
bool = true <|> false
where
true = bytes "True" $> True
false = bytes "False" $> False
{-# INLINABLE bool #-}
string :: P String
string = BS.unpack <$> doubleQuoted (takeWhile (/= '"'))
{-# INLINABLE string #-}
-- ------------------------------------------------------------------------
-- Tix parser
-- ------------------------------------------------------------------------
parseTix :: P Tix
parseTix = do
bytes "Tix" *> spaces
tix_modules <- bracketed (sepBy tixModule comma)
pure (Tix tix_modules)
tixModule :: P TixModule
tixModule = do
spaces *> bytes "TixModule" *> spaces
name <- string <* spaces
hash <- fmap toHash int <* spaces
size <- int <* spaces
ticks <- bracketed (sepBy integer comma)
pure (TixModule name hash size ticks)
-- ------------------------------------------------------------------------
-- Mix parser
-- ------------------------------------------------------------------------
parseMix :: P Mix
parseMix = do
bytes "Mix" *> spaces
path <- string <* spaces
_year <- takeWhile (/= ' ') <* spaces
_time <- takeWhile (/= ' ') <* spaces
_zone <- takeWhile (/= ' ') <* spaces
hash <- fmap toHash int <* spaces
tabstop <- int <* spaces
let dummy_date = UTCTime (fromOrdinalDate 1900 1) 0
Mix path dummy_date hash tabstop <$> mixEntries
mixEntries :: P [MixEntry]
mixEntries = bracketed (sepBy mixEntry comma)
{-# INLINABLE mixEntries #-}
mixEntry :: P MixEntry
mixEntry = parenthesized $ do
pos <- hpcPos
comma
box <- boxLabel
pure (pos, box)
{-# INLINABLE mixEntry #-}
hpcPos :: P HpcPos
hpcPos = do
sl <- int
char ':'
sc <- int
char '-'
el <- int
char ':'
ec <- int
pure (toHpcPos (sl, sc, el, ec))
{-# INLINABLE hpcPos #-}
boxLabel :: P BoxLabel
boxLabel = expBox <|> topLevelBox <|> localBox <|> binBox
where
expBox = bytes "ExpBox" *> spaces *> fmap ExpBox bool
topLevelBox = bytes "TopLevelBox" *> spaces *> fmap TopLevelBox names
localBox = bytes "LocalBox" *> spaces *> fmap LocalBox names
binBox = bytes "BinBox" *> spaces *>
(BinBox <$> (condBox <* spaces) <*> bool)
{-# INLINABLE boxLabel #-}
names :: P [String]
names = bracketed (sepBy string comma)
{-# INLINABLE names #-}
condBox :: P CondBox
condBox = guard <|> cond <|> qual
where
guard = bytes "GuardBinBox" $> GuardBinBox
cond = bytes "CondBinBox" $> CondBinBox
qual = bytes "QualBinBox" $> QualBinBox
{-# INLINABLE condBox #-}