{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TemplateHaskell #-}
module What4.Protocol.SMTLib2.Parse
(
CheckSatResponse(..)
, readCheckSatResponse
, GetModelResponse
, readGetModelResponse
, ModelResponse(..)
, DefineFun(..)
, Symbol
, Sort(..)
, pattern Bool
, pattern Int
, pattern Real
, pattern RoundingMode
, pattern Array
, Term(..)
) where
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail( MonadFail )
import qualified Control.Monad.Fail
#endif
import Control.Monad.Reader
import qualified Data.ByteString as BS
import qualified Data.ByteString.UTF8 as UTF8
import Data.Char
import Data.HashSet (HashSet)
import qualified Data.HashSet as HSet
import Data.Ratio
import Data.String
import Data.Word
import System.IO
c2b :: Char -> Word8
c2b = fromIntegral . fromEnum
newtype Parser a = Parser { unParser :: ReaderT Handle IO a }
deriving (Functor, Applicative)
instance Monad Parser where
Parser m >>= h = Parser $ m >>= unParser . h
#if !MIN_VERSION_base(4,13,0)
fail = Control.Monad.Fail.fail
#endif
instance MonadFail Parser where
fail = error
runParser :: Handle -> Parser a -> IO a
runParser h (Parser f) = runReaderT f h
parseChar :: Parser Char
parseChar = Parser $ ReaderT $ hGetChar
peekChar :: Parser Char
peekChar = Parser $ ReaderT $ hLookAhead
dropChar :: Parser ()
dropChar = Parser $ ReaderT $ \h -> hGetChar h *> pure ()
dropWhitespace :: Parser ()
dropWhitespace = do
c <- peekChar
if isSpace c then do
dropChar >> dropWhitespace
else
pure ()
matchChar :: Char -> Parser ()
matchChar expected = do
c <- parseChar
if c == expected then
pure ()
else if isSpace c then
matchChar expected
else
fail $ "Unexpected input char " ++ show c ++ "(expected " ++ show expected ++ ")"
matchString :: BS.ByteString -> Parser ()
matchString expected = do
dropWhitespace
found <- Parser $ ReaderT $ \h -> BS.hGet h (BS.length expected)
when (found /= expected) $ do
fail $ "Unexpected string " ++ show found ++ "(expected " ++ show expected ++ ")"
parseUntilCloseParen' :: [a] -> Parser a -> Parser [a]
parseUntilCloseParen' prev p = do
c <- peekChar
if isSpace c then
dropChar >> parseUntilCloseParen' prev p
else if c == ')' then
dropChar *> pure (reverse prev)
else do
p >>= \n -> parseUntilCloseParen' (n:prev) p
parseUntilCloseParen :: Parser a -> Parser [a]
parseUntilCloseParen = parseUntilCloseParen' []
takeChars' :: (Char -> Bool) -> [Word8] -> Parser [Word8]
takeChars' p prev = do
c <- peekChar
if p c then do
_ <- parseChar
takeChars' p (c2b c:prev)
else do
pure $! prev
takeChars :: (Char -> Bool) -> Parser BS.ByteString
takeChars p = do
l <- takeChars' p []
pure $! BS.pack (reverse l)
instance IsString (Parser ()) where
fromString = matchString . fromString
parseQuotedString :: Parser String
parseQuotedString = do
matchChar '"'
l <- takeChars (/= '"')
matchChar '"'
pure $ UTF8.toString l
class CanParse a where
parse :: Parser a
readFromHandle :: Handle -> IO a
readFromHandle h = runParser h parse
data CheckSatResponse
= SatResponse
| UnsatResponse
| UnknownResponse
| CheckSatUnsupported
| CheckSatError !String
instance CanParse CheckSatResponse where
parse = do
isParen <- checkParen
if isParen then do
matchString "error"
dropWhitespace
msg <- parseQuotedString
closeParen
pure (CheckSatError msg)
else
matchApp [ ("sat", pure SatResponse)
, ("unsat", pure UnsatResponse)
, ("unknown", pure UnknownResponse)
, ("unsupported", pure CheckSatUnsupported)
]
readCheckSatResponse :: Handle -> IO CheckSatResponse
readCheckSatResponse = readFromHandle
newtype Symbol = Symbol BS.ByteString
deriving (Eq)
instance Show Symbol where
show (Symbol s) = show s
instance IsString Symbol where
fromString = Symbol . fromString
symbolCharSet :: HashSet Char
symbolCharSet = HSet.fromList
$ ['A'..'Z']
++ ['a'..'z']
++ ['0'..'9']
++ ['~', '!', '@', '$', '%', '^', '&', '*', '_', '-', '+', '=', '<', '>', '.', '?', '/']
initialSymbolCharSet :: HashSet Char
initialSymbolCharSet = symbolCharSet `HSet.difference` (HSet.fromList ['0'..'9'])
generalReservedWords :: HashSet BS.ByteString
generalReservedWords = HSet.fromList $
[ "!"
, "_"
, "as"
, "BINARY"
, "DECIMAL"
, "exists"
, "HEXADECIMAL"
, "forall"
, "let"
, "match"
, "NUMERAL"
, "par"
, "STRING"
]
commandNames :: HashSet BS.ByteString
commandNames = HSet.fromList $
[ "assert"
, "check-sat"
, "check-sat-assuming"
, "declare-const"
, "declare-datatype"
, "declare-datatypes"
, "declare-fun"
, "declare-sort"
, "define-fun"
, "define-fun-rec"
, "define-sort"
, "echo"
, "exit"
, "get-assertions"
, "get-assignment"
, "get-info"
, "get-model"
, "get-option"
, "get-proof"
, "get-unsat-assumptions"
, "get-unsat-core"
, "get-value"
, "pop"
, "push"
, "reset"
, "reset-assertions"
, "set-info"
, "set-logic"
, "set-option"
]
reservedWords :: HashSet BS.ByteString
reservedWords = HSet.union generalReservedWords commandNames
instance CanParse Symbol where
parse = do
dropWhitespace
c0 <- peekChar
if c0 == '|' then do
r <- takeChars' (`notElem` ['|', '/']) [c2b c0]
ce <- peekChar
when (ce /= '|') $ do
fail $ "Unexpected character " ++ show ce ++ " inside symbol."
pure $! Symbol (BS.pack $ reverse (c2b ce:r))
else if HSet.member c0 initialSymbolCharSet then do
r <- BS.pack . reverse <$> takeChars' (`HSet.member` symbolCharSet) [c2b c0]
when (HSet.member r reservedWords) $ do
fail $ "Symbol cannot be reserved word " ++ show r
pure $! Symbol r
else do
fail $ "Unexpected character " ++ show c0 ++ " starting symbol."
matchApp :: [(BS.ByteString, Parser a)] -> Parser a
matchApp actions = do
dropWhitespace
let allowedChar c = 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || c == '-'
w <- takeChars allowedChar
case filter (\(m,_p) -> m == w) actions of
[] -> do
w' <- takeChars (\c -> c `notElem` ['\r', '\n'])
fail $ "Unsupported keyword: " ++ UTF8.toString (w <> w')
[(_,p)] -> p
_:_:_ -> fail $ "internal error: Duplicate keywords " ++ show w
openParen :: Parser ()
openParen = matchChar '('
closeParen :: Parser ()
closeParen = matchChar ')'
checkParen :: Parser Bool
checkParen = do
c <- peekChar
if c == '(' then
dropChar >> pure True
else if isSpace c then
parseChar >> checkParen
else
pure False
data Sort
= Sort Symbol [Sort]
| BitVec !Integer
| FloatingPoint !Integer !Integer
pattern Bool :: Sort
pattern Bool = Sort "Bool" []
pattern Int :: Sort
pattern Int = Sort "Int" []
pattern Real :: Sort
pattern Real = Sort "Real" []
pattern RoundingMode :: Sort
pattern RoundingMode = Sort "RoundingMode" []
pattern Array :: Sort -> Sort -> Sort
pattern Array x y = Sort "Array" [x,y]
parseDecimal' :: Integer -> Parser Integer
parseDecimal' cur = do
c <- peekChar
if '0' <= c && c <= '9' then do
dropChar
parseDecimal' $! 10 * cur + toInteger (fromEnum c - fromEnum '0')
else
pure cur
parseDecimal ::Parser Integer
parseDecimal = parseDecimal' 0
instance CanParse Integer where
parse = dropWhitespace *> parseDecimal
instance CanParse Sort where
parse = do
isParen <- checkParen
if isParen then do
sym <- parse
if sym == "_" then do
r <- matchApp [ (,) "BitVec" (BitVec <$> parse)
, (,) "FloatingPoint" (FloatingPoint <$> parse <*> parse)
]
closeParen
pure r
else
Sort sym <$> parseUntilCloseParen parse
else do
sym <- parse
pure $! Sort sym []
data Term
= SymbolTerm !Symbol
| AsConst !Sort !Term
| BVTerm !Integer !Integer
| IntTerm !Integer
| RatTerm !Rational
| StoreTerm !Term !Term !Term
| IfEqTerm !Symbol !Term !Term !Term
parseIntegerTerm :: Parser Integer
parseIntegerTerm = do
isParen <- checkParen
if isParen then do
matchString "-"
r <- parse
closeParen
pure $! negate r
else do
parse
parseEq :: Parser (Symbol, Term)
parseEq = do
openParen
matchString "="
var <- parse
val <- parse
closeParen
pure (var,val)
parseTermApp :: Parser Term
parseTermApp = do
dropWhitespace
isParen <- checkParen
if isParen then do
matchString "as"
matchString "const"
r <- AsConst <$> parse <*> parse
closeParen
pure $! r
else do
op <- parse :: Parser Symbol
case op of
"_" -> do
matchString "bv"
BVTerm <$> parseDecimal <*> parse
"/" -> do
num <- parseIntegerTerm
den <- parse
when (den == 0) $ fail $ "Model contains divide-by-zero"
pure $ RatTerm (num % den)
"-" -> do
IntTerm . negate <$> parse
"store" ->
StoreTerm <$> parse <*> parse <*> parse
"ite" -> do
(var,val) <- parseEq
t <- parse
f <- parse
pure $ IfEqTerm var val t f
_ -> do
fail $ "Unsupported operator symbol " ++ show op
instance CanParse Term where
parse = do
dropWhitespace
c <- peekChar
if c == '(' then do
t <- parseTermApp
closeParen
pure $! t
else if isDigit c then
IntTerm <$> parseDecimal
else if c == '@' then
SymbolTerm <$> parse
else
fail $ "Could not parse term"
data DefineFun = DefineFun { funSymbol :: !Symbol
, funArgs :: ![(Symbol, Sort)]
, funResultSort :: !Sort
, funDef :: !Term
}
data ModelResponse
= DeclareSortResponse !Symbol !Integer
| DefineFunResponse !DefineFun
parseSortedVar :: Parser (Symbol, Sort)
parseSortedVar = openParen *> ((,) <$> parse <*> parse) <* closeParen
parseDefineFun :: Parser DefineFun
parseDefineFun = do
sym <- parse
args <- openParen *> parseUntilCloseParen parseSortedVar
res <- parse
def <- parse
pure $! DefineFun { funSymbol = sym
, funArgs = args
, funResultSort = res
, funDef = def
}
instance CanParse ModelResponse where
parse = do
openParen
r <- matchApp [ (,) "declare-sort" $ DeclareSortResponse <$> parse <*> parse
, (,) "define-fun" $ DefineFunResponse <$> parseDefineFun
, (,) "define-fun-rec" $ fail "Do not yet support define-fun-rec"
, (,) "define-funs-rec" $ fail "Do not yet support define-funs-rec"
]
closeParen
pure $! r
type GetModelResponse = [ModelResponse]
readGetModelResponse :: Handle -> IO GetModelResponse
readGetModelResponse h =
runParser h $
openParen *> parseUntilCloseParen parse