language-smtlib-0.1.0.0: src/Language/SMTLIB/Parser/Internal.hs
-- | The megaparsec lexer and the grammar shared by terms and commands:
-- spec-constants, symbols, keywords, indices, identifiers, sorts, qualified
-- identifiers, s-expressions and attributes.
--
-- Every node parser is wrapped with 'withSpan', so each AST node is annotated
-- with the t'SrcSpan' it was parsed from. The annotation always occupies the
-- last field of a constructor, which means a fully-applied-but-for-the-span
-- constructor has type @t'SrcSpan' -> node t'SrcSpan'@ — exactly what 'withSpan'
-- consumes.
module Language.SMTLIB.Parser.Internal
( P
-- * Lexing
, sc
, lexeme
, withSpan
, tok
, openP
, closeP
, parens
, numeral
, pBool
, pStringLit
-- * Lexical tokens
, pSpecConstant
, pSymbolRaw
, pAnyWord
, pKeyword
-- * Shared grammar
, pIndex
, pIdentifier
, pSort
, pQualIdentifier
, pSExpr
, pAttribute
, pAttributeValue
) where
import Data.Char (isDigit, isHexDigit)
import Data.Functor (($>))
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Read as TR
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char (char, string)
import qualified Text.Megaparsec.Char.Lexer as L
import Language.SMTLIB.Internal.Lexical
(isSimpleSymbolChar, isSimpleSymbolStartChar, reservedWords)
import Language.SMTLIB.Syntax.Annotation (SrcSpan(..))
import Language.SMTLIB.Syntax.Attribute
import Language.SMTLIB.Syntax.Constant
import Language.SMTLIB.Syntax.Identifier
-- | The concrete parser monad: megaparsec over strict 'Text' with no custom
-- error component.
type P = Parsec Void Text
-- | The whitespace consumer: spaces and @;@ line comments (SMT-LIB has no block
-- comments).
sc :: P ()
sc = L.space spaceChars (L.skipLineComment ";") empty
where spaceChars = () <$ takeWhile1P (Just "white space") isWs
isWs c = c == ' ' || c == '\t' || c == '\n' || c == '\r'
-- | Run a parser then consume trailing whitespace\/comments.
lexeme :: P a -> P a
lexeme = L.lexeme sc
-- | Annotate a node with the source span it covers. The supplied parser yields
-- a constructor still awaiting its final t'SrcSpan' field.
withSpan :: P (SrcSpan -> a) -> P a
withSpan p = do
s <- getOffset
f <- p
e <- getOffset
pure (f (SrcSpan s e))
-- | Match a fixed token (keyword or reserved word), ensuring it is not merely a
-- prefix of a longer symbol.
tok :: Text -> P ()
tok w = (lexeme . try) (string w *> notFollowedBy (satisfy isSimpleSymbolChar)) $> ()
-- | An opening parenthesis (and trailing whitespace).
openP :: P ()
openP = lexeme (char '(') $> ()
-- | A closing parenthesis (and trailing whitespace).
closeP :: P ()
closeP = lexeme (char ')') $> ()
-- | @parens p@ parses @p@ between a balanced pair of parentheses.
parens :: P a -> P a
parens p = openP *> p <* closeP
-- | A numeral (sequence of digits).
numeral :: P Integer
numeral = lexeme (readInteger <$> takeWhile1P (Just "digit") isDigit)
-- | Parse a non-empty run of decimal digits to an 'Integer', reading directly
-- from the 'Text' (no intermediate 'String'). Only ever called on input that
-- the caller has already constrained to one or more digits, so the error case
-- is unreachable.
readInteger :: Text -> Integer
readInteger t = case TR.decimal t of
Right (n, _) -> n
Left e -> error ("readInteger: " ++ e ++ ": " ++ T.unpack t)
-- | @true@ \/ @false@.
pBool :: P Bool
pBool = (tok "true" $> True) <|> (tok "false" $> False)
isBinDigit :: Char -> Bool
isBinDigit c = c == '0' || c == '1'
-- | A @spec_constant@. Numeric literals keep their raw lexeme so that printing
-- round-trips exactly.
pSpecConstant :: P (SpecConstant SrcSpan)
pSpecConstant = withSpan (lexeme (pHash <|> pStr <|> pNumDec))
where
pHash = char '#' *> (pHex <|> pBin)
pHex = char 'x' *> (SCHexadecimal <$> takeWhile1P (Just "hex digit") isHexDigit)
pBin = char 'b' *> (SCBinary <$> takeWhile1P (Just "binary digit") isBinDigit)
pStr = SCString <$> pStringBody
pNumDec = do
intp <- takeWhile1P (Just "digit") isDigit
mfrac <- optional (char '.' *> takeWhileP (Just "digit") isDigit)
pure $ case mfrac of
Nothing -> SCNumeral (readInteger intp)
Just fr -> SCDecimal (T.concat [intp, ".", fr])
-- | The body of a string literal (no trailing whitespace), decoding @""@ into a
-- single quote.
pStringBody :: P Text
pStringBody = char '"' *> go
where
go = do
seg <- takeWhileP (Just "string char") (/= '"')
_ <- char '"'
mq <- optional (char '"')
case mq of
Just _ -> (\rest -> T.concat [seg, "\"", rest]) <$> go
Nothing -> pure seg
-- | A string literal as a lexeme.
pStringLit :: P Text
pStringLit = lexeme pStringBody
-- | A symbol (simple or @|...|@ quoted), returning its logical value. Simple
-- symbols that are reserved words are rejected so the grammar's keywords are
-- not swallowed.
pSymbolRaw :: P Symbol
pSymbolRaw = lexeme (pSimple <|> pQuoted)
where
pSimple = try $ do
h <- satisfy isSimpleSymbolStartChar
t <- takeWhileP (Just "symbol char") isSimpleSymbolChar
let s = T.cons h t
if s `Set.member` reservedWords
then fail ("reserved word " ++ T.unpack s)
else pure s
pQuoted = quotedBody
-- | A @|...|@ quoted symbol body, returning its logical value.
quotedBody :: P Text
quotedBody = char '|' *> takeWhileP (Just "quoted-symbol char") (\c -> c /= '|' && c /= '\\') <* char '|'
-- | A simple word /including/ reserved words (used inside s-expressions).
pAnyWord :: P Text
pAnyWord = lexeme $ do
h <- satisfy isSimpleSymbolStartChar
t <- takeWhileP (Just "symbol char") isSimpleSymbolChar
pure (T.cons h t)
-- | A keyword, returned without its leading colon.
pKeyword :: P Keyword
pKeyword = lexeme (char ':' *> takeWhile1P (Just "keyword char") isSimpleSymbolChar)
-- | An @index@: a numeral or a symbol.
pIndex :: P (Index SrcSpan)
pIndex = withSpan ((IxNumeral <$> numeral) <|> (IxSymbol <$> pSymbolRaw))
-- | An @identifier@: a symbol, or @(_ symbol index+)@.
pIdentifier :: P (Identifier SrcSpan)
pIdentifier = withSpan (plain <|> indexed)
where
plain = (\s -> Identifier s []) <$> pSymbolRaw
indexed = do
_ <- openP
_ <- tok "_"
s <- pSymbolRaw
ixs <- some pIndex
_ <- closeP
pure (Identifier s ixs)
-- | A @sort@: an identifier, or @(identifier sort+)@.
pSort :: P (Sort SrcSpan)
pSort = withSpan (try simple <|> param)
where
simple = (\i -> Sort i []) <$> pIdentifier
param = parens (Sort <$> pIdentifier <*> some pSort)
-- | A @qual_identifier@: an identifier or @(as identifier sort)@.
pQualIdentifier :: P (QualIdentifier SrcSpan)
pQualIdentifier = withSpan (try asForm <|> plain)
where
asForm = parens (tok "as" *> (QIdentifierAs <$> pIdentifier <*> pSort))
plain = QIdentifier <$> pIdentifier
-- | An @s_expr@.
pSExpr :: P (SExpr SrcSpan)
pSExpr = withSpan $ choice
[ SEConstant <$> pSpecConstant
, SEKeyword <$> pKeyword
, SEList <$> parens (many pSExpr)
, wordOrReserved
, SESymbol <$> lexeme quotedBody
]
where
wordOrReserved = do
w <- pAnyWord
pure (if w `Set.member` reservedWords then SEReserved w else SESymbol w)
-- | An @attribute_value@.
pAttributeValue :: P (AttributeValue SrcSpan)
pAttributeValue = withSpan $ choice
[ AVConstant <$> pSpecConstant
, AVSExpr <$> parens (many pSExpr)
, AVSymbol <$> pSymbolRaw
]
-- | An @attribute@: a keyword, optionally followed by a value.
pAttribute :: P (Attribute SrcSpan)
pAttribute = withSpan $ do
k <- pKeyword
mv <- optional pAttributeValue
pure (maybe (Attribute k) (AttributeWith k) mv)