{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE FlexibleContexts #-}
-- TODO: Hackage, github, haddock, tests
-- TODO: isGzipped to a personal utility package; readAndDecompress
-- like readFile but if Gzipped will also decompress
-- TODO: Only works with parseOnly beause I decided to do special handling of
-- putting this into statements so trailing space at the end of the file will
-- make it return "Partial _"
-- TODO: keep track of line numbers in case of failure
module Data.Avc.Parser (parseAvcFile) where
import Codec.Compression.GZip
import qualified Data.ByteString.Lazy.Char8 as BL
import Control.DeepSeq
import Control.DeepSeq.Generics (genericRnf)
import GHC.Generics hiding (Rep)
import Data.ByteString.Char8 as BS hiding (split)
import Control.Monad
import Control.Applicative hiding (many)
import Data.Attoparsec.ByteString.Char8 as A
import qualified System.IO as IO
import qualified Test.HUnit as H
import qualified Data.List as DL hiding (words, unwords, takeWhile, elem, lines)
import Data.Function (on)
import Data.List.Split (keepDelimsR, whenElt, split)
import Prelude hiding (lines, elem, null, unwords, map, null)
import qualified Prelude as P
import Data.Char as DC (isLetter, isDigit)
import Data.Binary.Get (runGet, getWord16le)
import Data.Avc.Type
-- | Parse a simple AVC which may be gzipped
-- Only handles simple files with one character per state like:
--
-- # Begin AVC
-- FORMAT a b c;
-- R10 10X ; vector comment # other comment
-- R10 01H ;
parseAvcFile :: FilePath -> IO [Statement]
parseAvcFile filename = do
content <- decompressFile filename
let strict = BL.toStrict content
let ls = BS.lines strict
let ncmnt = nocomment ls
let stmts = joinStatements ncmnt
return $ getStatements stmts
-- TODO: put this in my utility module
decompressFile :: FilePath -> IO BL.ByteString
decompressFile filename = do
bs <- BL.readFile filename
isgz <- isGzipped bs
let decompressed = decompress bs
return $ if isgz then decompressed else bs
isGzipped :: BL.ByteString -> IO Bool
isGzipped bs = do
let magic = runGet getWord16le bs
return $ magic == 0x8b1f
-- /utility
-- | Strip # comments from a list of lines
nocomment :: [ByteString] -> [ByteString]
nocomment = P.map dropComment
where dropComment bs = let idx = elemIndex '#' bs in
case idx of
Nothing -> bs
Just i -> BS.take i bs
-- | Join statements terminated by ';' into one line each also keeping
-- info after ';' which is a comment on the statement.
joinStatements :: [ByteString] -> [ByteString]
joinStatements bss = P.map unwords (grouped bss)
where grouped = split (keepDelimsR $ whenElt hasSemi)
hasSemi hs = ';' `elem` hs
-- Assume first statement is Format
getFormat :: ByteString -> Statement
getFormat bs =
case parseOnly (skipSpace *> parseFormat) bs of
Left e -> error ("Error parsing FORMAT: " ++ show e ++ "\nInput:" ++ show bs)
Right r -> r
getStatements :: [ByteString] -> [Statement]
getStatements = P.map getStatement
-- getStatement "R1 cyc 123" -- TODO: doesn't return. some kind of inifinite loop without the ';'
getStatement bs =
case parseOnly (skipSpace *> parseStatement) bs of
Left e -> error ("Error parsing FORMAT: " ++ show e ++ "\nInput:" ++ show bs)
Right r -> r
parseStatement :: Parser Statement
parseStatement =
choice [parseRepeat
,parseFormat
,parseEof]
parseEof :: Parser Statement
parseEof = do
endOfInput
return EOF
-- Parser A is like parser b but it skips trailing spaces
-- TODO: returns "Partial _" if space until EOF
lexeme :: Parser a -> Parser a
lexeme pa = pa <* skipSpace
parseRepeat :: Parser Statement
parseRepeat = Repeat <$> (keyword "R" *> rep) <*> devcyc <*> parseVec <*> parseComment
where
rep = lexeme decimal
devcyc = lexeme $ takeWhile1 isVecChar
isVecChar :: Char -> Bool
isVecChar c = A.isAlpha_ascii c || A.isDigit c
{-# INLINE isVecChar #-}
parseVec :: Parser ByteString
parseVec = do
vec <- manyTill (skipSpace *> A.takeWhile isVecChar) (skipSpace *> semicolon)
return $ BS.concat vec
parseFail :: String -> ByteString -> [String] -> String -> Statement
parseFail usrmsg trying contexts errmsg = error message
where message = "Fail parsing: " ++ unpack trying
++ "\nError message: " ++ errmsg
++ "\nContexts: " ++ DL.intercalate "\n+" contexts
parseLeftover :: String -> ByteString -> Statement
parseLeftover trying leftover = error $ "Leftover input trying: " ++ trying ++ " Leftover: '" ++ unpack leftover ++ "'"
parseFormat' :: Parser Statement
parseFormat' = Format <$> (keyword "FORMAT" *> sigs) <*> parseComment <?> "FORMAT"
where
sigs = manyTill parseSig semicolon
parseFormat :: Parser Statement
parseFormat = do
linespaces
parseFormat'
parseSig :: Parser Signal
parseSig = Sig <$> lexeme (takeWhile1 isSigChar)
letter = letter_ascii
parseComment :: Parser Comment
parseComment = do
cmnt <- A.takeWhile (const True) <* endOfInput
return $ Comment cmnt
linespaces :: Parser ()
linespaces = void $ A.takeWhile (`elem` "\t ")
isSigChar :: Char -> Bool
isSigChar c = isLetter c || DC.isDigit c || c `elem` "[]:_"
semicolon :: Parser ()
semicolon = void $ lexeme $ char ';'
keyword :: ByteString -> Parser ()
keyword kw = void $ lexeme $ string kw