packages feed

candid-0.2: src/Codec/Candid/Parse.hs

{-# LANGUAGE DeriveTraversable #-}
module Codec.Candid.Parse
  ( DidFile(..)
  , DidDef
  , TypeName
  , parseDid
  , parseDidType
  , parseValue
  , parseValues
  , CandidTestFile(..)
  , CandidTest(..)
  , TestInput(..)
  , TestAssertion(..)
  , parseCandidTests
  )  where

import qualified Data.ByteString.Lazy as BS
import qualified Data.Text.Encoding as T
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Set as Set
import Text.Megaparsec
import Text.Megaparsec.Char
import Control.Applicative.Permutations
import Data.Bifunctor
import Data.Char
import Data.Functor
import Data.Word
import Numeric.Natural
import Numeric
import Control.Monad
import Data.Void
import Text.Read (readMaybe)
import Data.Scientific

import Codec.Candid.Data
import Codec.Candid.Types
import Codec.Candid.FieldName

type Parser = Parsec Void String

-- | Parses a Candid description (@.did@) from a string
parseDid :: String -> Either String DidFile
parseDid = first errorBundlePretty . parse (allInput fileP) "Candid service"

parseDidType :: String -> Either String (Type TypeName)
parseDidType = first errorBundlePretty . parse (allInput dataTypeP) "Candid type"

-- | Parses a Candid textual value from a string
parseValue :: String -> Either String Value
parseValue = first errorBundlePretty . parse (allInput valueP) "Candid value"

-- | Parses a sequence of  Candid textual values from a string
parseValues :: String -> Either String [Value]
parseValues = first errorBundlePretty . parse (allInput valuesP) "Candid values (argument sequence)"

allInput :: Parser a -> Parser a
allInput = between theVoid eof

fileP :: Parser DidFile
fileP = DidFile <$> defsP <*> actorP

defsP :: Parser [DidDef TypeName]
defsP = concat <$> many defP

defP :: Parser [DidDef TypeName]
defP = (typeP <|> importP) <* s ";"

typeP :: Parser [DidDef TypeName]
typeP = fmap (:[]) $
    (,) <$ k "type" <*> idP <* s "=" <*> dataTypeP

importP :: Parser [DidDef TypeName]
importP = withPredicate (const (Left "imports not yet supported")) $
    [] <$ k "import"

actorP :: Parser (DidService TypeName)
actorP = k "service" *> optional idP *> s ":" *> actorTypeP -- TODO could be a type id

actorTypeP :: Parser (DidService TypeName)
actorTypeP = braceSemi methP

methP :: Parser (T.Text, MethodType TypeName)
methP = (,) <$> nameP <* s ":" <*> funcTypeP

funcTypeP :: Parser (MethodType TypeName)
funcTypeP = do
    ts1 <- seqP
    s "->"
    ts2 <- seqP
    (q,o) <- runPermutation $
         (,) <$> toPermutationWithDefault False (True <$ s "query")
             <*> toPermutationWithDefault False (True <$ s "oneway")
    return $ MethodType ts1 ts2 q o

nameP :: Parser T.Text
nameP = textP <|> idP <?> "name"

textP :: Parser T.Text
textP = T.pack <$> l (between (char '"') (char '"') (many stringElem)) <?> "text"

blobP :: Parser BS.ByteString
blobP = BS.concat <$> l (between (char '"') (char '"') (many blobElem)) <?> "blob"

blobElem :: Parser BS.ByteString
blobElem = choice
    [ try (char '\\' *> lookAhead hexdigit) *> do
        raw <- replicateM 2 hexdigit
        case readHex raw of
            [(n,"")] -> return (BS.singleton (fromIntegral (n::Integer)))
            _ -> fail "Internal parsing error parsing hex digits"
    , BS.fromStrict . T.encodeUtf8 . T.singleton <$> stringElem
    ]

stringElem :: Parser Char
stringElem = (char '\\' *> go) <|> noneOf "\""
  where
    go :: Parser Char
    go = choice
        [ '\t' <$ char 't'
        , '\n' <$ char 'n'
        , '\r' <$ char 'r'
        , '\"' <$ char '\"'
        , '\'' <$ char '\''
        , '\\' <$ char '\\'
        , between (string "u{") (string "}") hexnum
        ]

    hexnum :: Parser Char
    hexnum = do
        raw <- concat <$> some (replicateM 2 hexdigit)
        case readHex raw of
            [(n,"")] -> return (chr n)
            _ -> fail $ "Invalid hex string " ++ show raw

hexdigit :: Parser Char
hexdigit = oneOf "0123456789ABCDEFabcdef"

seqP :: Parser [Type TypeName]
seqP = parenComma argTypeP

argTypeP :: Parser (Type TypeName)
argTypeP = (try (nameP *> s ":") *> dataTypeP) <|> dataTypeP

dataTypeP :: Parser (Type TypeName)
dataTypeP = primTypeP <|> constTypeP <|> refTypeP <|> (RefT <$> idP)-- TODO: reftypes

primTypeP :: Parser (Type TypeName)
primTypeP = choice
    [ NatT <$ k "nat"
    , Nat8T <$ k "nat8"
    , Nat16T <$ k "nat16"
    , Nat32T <$ k "nat32"
    , Nat64T <$ k "nat64"
    , IntT <$ k "int"
    , Int8T <$ k "int8"
    , Int16T <$ k "int16"
    , Int32T <$ k "int32"
    , Int64T <$ k "int64"
    , Float32T <$ k "float32"
    , Float64T <$ k "float64"
    , BoolT <$ k "bool"
    , TextT <$ k "text"
    , NullT <$ k "null"
    , ReservedT <$ k "reserved"
    , EmptyT <$ k "empty"
    , BlobT <$ k "blob"
    , PrincipalT <$ k "principal"
    ]

constTypeP :: Parser (Type TypeName)
constTypeP = choice
  [ OptT <$ k "opt" <*> dataTypeP
  , VecT <$ k "vec" <*> dataTypeP
  , RecT . resolveShorthand <$ k "record" <*> braceSemi recordFieldTypeP
  , VariantT <$ k "variant" <*> braceSemi variantFieldTypeP
  ]

refTypeP :: Parser (Type TypeName)
refTypeP = choice
    [ FuncT <$ k "func" <*> funcTypeP
    , ServiceT <$ k "service" <*> actorTypeP
    ]

fieldLabelP :: Parser FieldName
fieldLabelP  =
    hashedField . fromIntegral <$> natP <|>
    labledField <$> nameP

variantFieldTypeP :: Parser (FieldName, Type TypeName)
variantFieldTypeP =
  (,) <$> fieldLabelP <*> ((s ":" *> dataTypeP) <|> pure NullT)

resolveShorthand :: [Word32 -> (FieldName, a)] -> [(FieldName, a)]
resolveShorthand = go 0
  where
    go _ [] = []
    go n (f:fs) =
        let f' = f n in
        f' : go (succ (fieldHash (fst f'))) fs

recordFieldTypeP :: Parser (Word32 -> (FieldName, Type TypeName))
recordFieldTypeP = choice
  [ try $ do
    l <- fieldLabelP
    s ":"
    t <- dataTypeP
    return $ const (l,t)
  , do
    t <- dataTypeP
    return $ \next -> (hashedField next, t)
  ]

idP :: Parser T.Text
idP = T.pack <$> l ((:)
  <$> satisfy (\c -> isAscii c && isLetter c || c == '_')
  <*> many (satisfy (\c -> isAscii c && isAlphaNum c || c == '_'))
  ) <?> "id"

valuesP :: Parser [Value]
valuesP = (parenComma annValueP <?> "argument sequence")
       <|> ((:[]) <$> annValueP) -- for convenience

annValueP :: Parser Value
annValueP =
  parens annValueP <|> do -- this parser allows extra parentheses
      v <- valueP
      s ":" *> do
            t <- dataTypeP
            smartAnnV v t
       <|> return v

smartAnnV :: Value -> Type TypeName -> Parser Value
smartAnnV (NumV n) Nat8T = Nat8V <$> toBounded n
smartAnnV (NumV n) Nat16T = Nat16V <$> toBounded n
smartAnnV (NumV n) Nat32T = Nat32V <$> toBounded n
smartAnnV (NumV n) Nat64T = Nat64V <$> toBounded n
smartAnnV (NumV n) Int8T = Int8V <$> toBounded n
smartAnnV (NumV n) Int16T = Int16V <$> toBounded n
smartAnnV (NumV n) Int32T = Int32V <$> toBounded n
smartAnnV (NumV n) Int64T = Int64V <$> toBounded n
smartAnnV (NumV n) Float32T = return $ Float32V $ toRealFloat n
smartAnnV (NumV n) Float64T = return $ Float64V $ toRealFloat n
smartAnnV v ReservedT = return $ AnnV v ReservedT
smartAnnV _ _ = fail "Annotations are only supported around number literals"

toBounded :: (Integral a, Bounded a) => Scientific -> Parser a
toBounded v = maybe err return $ toBoundedInteger v
  where err = fail $ "Number literal out of bounds: " ++ show v

numP :: Parser Scientific
numP = l p >>= conv <?> "number"
  where
    p =(:) <$> oneOf "-+0123456789" <*> many (oneOf "-+.0123456789eE_")
    conv raw = case readMaybe (filter (/= '_') (handle_trailing_perdiod raw)) of
        Nothing -> fail $ "Invald number literal: " ++ show raw
        Just s -> return s
    -- 1. is allowed by candid, but not by scientific
    handle_trailing_perdiod s =
        if not (null s) && last s == '.' then s ++ "0" else s

valueP :: Parser Value
valueP = choice
  [ parens annValueP
  , NumV <$> numP
  , BoolV True <$ k "true"
  , BoolV False <$ k "false"
  , TextV <$> textP
  , NullV <$ k "null"
  , OptV . Just <$ k "opt" <*> valueP
  , VecV . V.fromList <$ k "vec" <*> braceSemi annValueP
  , RecV . resolveShorthand <$ k "record" <*> braceSemi recordFieldValP
  , uncurry VariantV <$ k "variant" <*> braces variantFieldValP
  , FuncV <$ k "func" <*> withPredicate parsePrincipal textP <* s "." <*> nameP
  , ServiceV <$ k "service" <*> withPredicate parsePrincipal textP
  , PrincipalV <$ k "principal" <*> withPredicate parsePrincipal textP
  , BlobV <$ k "blob" <*> blobP
  ]

variantFieldValP :: Parser (FieldName, Value)
variantFieldValP = (,) <$> fieldLabelP <*> ((s "=" *> annValueP) <|> pure NullV)

recordFieldValP :: Parser (Word32 -> (FieldName, Value))
recordFieldValP = choice
  [ try $ do
    l <- fieldLabelP
    s "="
    v <- annValueP
    return $ const (l,v)
  , do
    v <- annValueP
    return $ \next -> (hashedField next, v)
  ]

-- A lexeme
l :: Parser a -> Parser a
l x = x <* theVoid

-- The space between a lexeme
theVoid :: Parser ()
theVoid = void $ many (space1 <|> comment)

comment :: Parser ()
comment = lineComment <|> multiLineComment

-- a parser for nested multi-line comments. there might be a nicer way
multiLineComment :: Parser ()
multiLineComment = between (string "/*") (string "*/") $
    void $ many $
        multiLineComment <|>
        try (try $ char '*' *> notFollowedBy (char '/')) <|>
        void (anySingleBut '*')

lineComment :: Parser ()
lineComment = do
    void (string "//")
    void (takeWhileP (Just "character") (/= '\n'))
    void (char '\n')

-- a symbol
s :: String -> Parser ()
s str = void (l (string str)) <?> str

-- a keyword
k :: String -> Parser ()
k str = try (void (l (string str <* no)) <?> str)
  where
    no = notFollowedBy (satisfy (\c -> isAscii c && isAlphaNum c || c == '_'))

natP :: Parser Natural
natP = l (read <$> some digitChar <?> "number")

braces :: Parser a -> Parser a
braces = between (s "{") (s "}")
braceSemi :: Parser a -> Parser [a]
braceSemi p = braces $ sepEndBy p (s ";")
parens :: Parser a -> Parser a
parens = between (s "(") (s ")")
parenComma :: Parser a -> Parser [a]
parenComma p = parens $ sepEndBy p (s ",")


-- from https://markkarpov.com/tutorial/megaparsec.html#parse-errors
withPredicate :: (a -> Either String b) -> Parser a -> Parser b
withPredicate f p = do
  o <- getOffset
  r <- p
  case f r of
    Left msg -> parseError (FancyError o (Set.singleton (ErrorFail msg)))
    Right x -> return x


-- | A candid test file
--
-- (no support for type definitions yet)
data CandidTestFile = CandidTestFile
    { testDefs :: [ DidDef TypeName ]
    , testTests ::  [ CandidTest TypeName ]
    }

data CandidTest a = CandidTest
    { testLine :: Int
    , testAssertion :: TestAssertion
    , testType :: [Type a]
    , testDesc :: Maybe T.Text
    }
  deriving (Functor, Foldable, Traversable)

data TestInput
    = FromTextual T.Text
    | FromBinary BS.ByteString

data TestAssertion
    = CanParse TestInput
    | CannotParse TestInput
    | ParseEq Bool TestInput TestInput

-- | Parses a candid spec test file from a string
parseCandidTests :: String -> String -> Either String CandidTestFile
parseCandidTests source = first errorBundlePretty . parse (allInput testFileP) source

testFileP :: Parser CandidTestFile
testFileP = CandidTestFile <$> defsP <*> sepEndBy testP (s ";")

testP :: Parser (CandidTest TypeName)
testP = CandidTest
    <$> (unPos . sourceLine <$> getSourcePos)
    <*  k "assert"
    <*> testAssertP
    <*> seqP
    <*> optional textP

testAssertP :: Parser TestAssertion
testAssertP = do
    input1 <- testInputP
    choice
        [ CanParse input1 <$ s ":"
        , CannotParse input1 <$ s "!:"
        , ParseEq True input1 <$ s "==" <*> testInputP <* s ":"
        , ParseEq False input1 <$ s "!=" <*> testInputP <* s ":"
        ]

testInputP :: Parser TestInput
testInputP = FromTextual <$> textP <|> FromBinary <$> (k "blob" *> blobP)