{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Main (main) where
import Prelude hiding ((.), id)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Category
import Data.ByteString.Lazy.UTF8 (fromString)
import Data.Char
import Data.Scientific
import Data.Semigroup
import qualified Data.Set as S
import qualified Data.Text as TS
import Data.Text.Prettyprint.Doc (Pretty, pretty)
import GHC.Generics
import Test.QuickCheck ()
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC
import Language.Sexp.Located as Sexp
import Language.Sexp () -- for Show instance
import Data.InvertibleGrammar (ErrorMessage(..), runGrammar, forward, backward)
import Language.SexpGrammar as G
import Language.SexpGrammar.Generic
import Language.SexpGrammar.TH hiding (match)
import Debug.Trace
parseSexp' :: String -> Either String Sexp
parseSexp' input = Sexp.decode (fromString input)
instance Arbitrary Atom where
arbitrary = oneof
[ AtomNumber . fromFloatDigits <$> (arbitrary :: Gen Double)
, AtomNumber . fromIntegral <$> (arbitrary :: Gen Integer)
, AtomString . TS.pack <$> listOf
(oneof [ elements $ ['\n','\r','\t','"','\\', ' ']
, arbitrary `suchThat` (\c -> isAlphaNum c || isPunctuation c)
])
, AtomSymbol . TS.pack <$>
listOf (arbitrary `suchThat` (\c -> isAlphaNum c || c `elem` ("#',`\\:@!$%&*/<=>?~_^.|+-" :: [Char])))
`suchThat` (\s -> not $ all isDigit (drop 1 s) || null s || all (`elem` ("#',`" :: [Char])) (take 1 s))
, pure (AtomSymbol ":foo")
, pure (AtomSymbol "1e2")
, pure (AtomSymbol "-1e2")
, pure (AtomSymbol "1.0e-2")
, pure (AtomSymbol "+.0E-2")
, pure (AtomSymbol "bar")
, pure (AtomSymbol "~qux")
, pure (AtomSymbol "символ")
, pure (AtomSymbol "@baz")
]
instance Arbitrary Prefix where
arbitrary = elements
[ Quote
, Backtick
, Comma
, CommaAt
, Hash
]
instance Arbitrary Sexp where
arbitrary =
frequency
[ (3, Atom <$> arbitrary)
, (1, ParenList <$> scale (`div` 2) (listOf arbitrary))
, (1, BracketList <$> scale (`div` 2) (listOf arbitrary))
, (1, BraceList <$> scale (`div` 2) (listOf arbitrary))
, (1, Modified <$> arbitrary <*> (arbitrary `suchThat` (\case {Symbol s -> not ("@" `TS.isPrefixOf` s); _other -> True})))
]
shrink = \case
Atom a -> map Atom (shrink a)
ParenList [x] -> shrink x
ParenList xs -> map ParenList (shrinkList shrink xs)
BracketList [x] -> shrink x
BracketList xs -> map BracketList (shrinkList shrink xs)
BraceList [x] -> shrink x
BraceList xs -> map BraceList (shrinkList shrink xs)
Modified m s -> shrink s ++ s : Modified m (Symbol "foo") : map (Modified m) (shrink s)
other -> [other]
fromSexp' :: SexpGrammar a -> Sexp.Sexp -> Either (ErrorMessage Position) a
fromSexp' g = runGrammar Sexp.dummyPos . forward (G.sealed g)
toSexp' :: SexpGrammar a -> a -> Either (ErrorMessage Position) Sexp.Sexp
toSexp' g = runGrammar Sexp.dummyPos . backward (G.sealed g)
data Pair a b = Pair a b
deriving (Show, Eq, Ord, Generic)
instance (Arbitrary a, Arbitrary b) => Arbitrary (Pair a b) where
arbitrary = Pair <$> arbitrary <*> arbitrary
data Foo a b
= Bar a b
| Baz a b
deriving (Show, Eq, Ord, Generic)
instance (Arbitrary a, Arbitrary b) => Arbitrary (Foo a b) where
arbitrary =
frequency
[ (1, Bar <$> arbitrary <*> arbitrary)
, (1, Baz <$> arbitrary <*> arbitrary)
]
data ArithExpr =
Lit Int
| Add ArithExpr ArithExpr -- ^ (+ x y)
| Mul [ArithExpr] -- ^ (* x1 ... xN)
deriving (Show, Eq, Ord, Generic)
return []
string' :: Grammar Position (Sexp :- t) (String :- t)
string' = string >>> iso TS.unpack TS.pack
instance Arbitrary ArithExpr where
arbitrary = frequency
[ (5, Lit <$> arbitrary)
, (1, Add <$> arbitrary <*> arbitrary)
, (1, do
n <- choose (0, 7)
Mul <$> vectorOf n arbitrary)
]
instance (SexpIso a, SexpIso b) => SexpIso (Pair a b) where
sexpIso = $(grammarFor 'Pair) . list (el sexpIso >>> el sexpIso)
pairGenericIso
:: (forall t. Grammar Position (Sexp :- t) (a :- t))
-> (forall t. Grammar Position (Sexp :- t) (b :- t)) -> Grammar Position (Sexp :- t) (Pair a b :- t)
pairGenericIso a b = with (\pair -> pair . list (el a >>> el b))
instance (SexpIso a, SexpIso b) => SexpIso (Foo a b) where
sexpIso = sconcat
[ $(grammarFor 'Bar) . list (el (sym "bar") >>> el sexpIso >>> el sexpIso)
, $(grammarFor 'Baz) . list (el (sym "baz") >>> el sexpIso >>> el sexpIso)
]
fooGenericIso
:: (forall t. Grammar Position (Sexp :- t) (a :- t))
-> (forall t. Grammar Position (Sexp :- t) (b :- t)) -> Grammar Position (Sexp :- t) (Foo a b :- t)
fooGenericIso a b = match
$ With (\bar -> bar . list (el (sym "bar") >>> el a >>> el b))
$ With (\baz -> baz . list (el (sym "baz") >>> el a >>> el b))
$ End
arithExprTHIso :: Grammar Position (Sexp :- t) (ArithExpr :- t)
arithExprTHIso =
sconcat
[ $(grammarFor 'Lit) . int
, $(grammarFor 'Add) . list (el (sym "+") >>> el arithExprTHIso >>> el arithExprTHIso)
, $(grammarFor 'Mul) . list (el (sym "*") >>> rest arithExprTHIso)
]
arithExprGenericIso :: Grammar Position (Sexp :- t) (ArithExpr :- t)
arithExprGenericIso = expr
where
expr :: Grammar Position (Sexp :- t) (ArithExpr :- t)
expr = match
$ With (\lit -> lit . int)
$ With (\add -> add . list (el (sym "+") >>> el expr >>> el expr))
$ With (\mul -> mul . list (el (sym "*") >>> rest expr))
$ End
data Person = Person
{ _pName :: String
, _pAge :: Int
, _pAddress :: String
, _pChildren :: [Person]
} deriving (Show, Eq, Generic)
instance Arbitrary Person where
arbitrary =
Person
<$> arbitrary
<*> arbitrary
<*> arbitrary
<*> frequency
[ (6, pure [])
, (4, vectorOf 1 arbitrary)
, (2, vectorOf 2 arbitrary)
, (1, vectorOf 3 arbitrary)
]
personGenericIso :: Grammar Position (Sexp :- t) (Person :- t)
personGenericIso = with
(\person ->
list (
el (sym "person") >>>
el string' >>>
props (
":age" .: int >>>
":address" .: string') >>>
rest personGenericIso) >>> person)
----------------------------------------------------------------------
-- Test cases
allTests :: TestTree
allTests = testGroup "All tests"
[ lexerTests
, QC.testProperty "Format/decode invertibility"
(\a -> case Sexp.decode (Sexp.format a) of
Left _ -> trace "Cannot parse" False
Right b -> if toSimple a == toSimple b then True else trace ("Parsed " ++ show b) False)
, grammarTests
]
sexpEq :: (Pretty e, Eq e) => Either e Sexp -> Either e Sexp -> Assertion
sexpEq a b =
fmap toSimple a `otherEq` fmap toSimple b
otherEq :: (Pretty e, Eq e, Show a, Eq a) => Either e a -> Either e a -> Assertion
otherEq a b = do
(flip assertBool) (a == b) $
unlines
["Output mismatch:"
, ppOutput a
, "vs."
, ppOutput b
]
where
ppOutput o = case o of
Left err -> "Error message: " ++ show (pretty err)
Right v -> "Output: " ++ show v
lexerTests :: TestTree
lexerTests = testGroup "Sexp lexer/parser tests"
[ testCase "123 is an integer number" $
parseSexp' "123"
`sexpEq` Right (Number 123)
, testCase "+123 is an integer number" $
parseSexp' "+123"
`sexpEq` Right (Number 123)
, testCase "-123 is an integer number" $
parseSexp' "-123"
`sexpEq` Right (Number (- 123))
, testCase "+123.45 is a floating number" $
parseSexp' "+123.45"
`sexpEq` Right (Number (read "123.45" :: Scientific))
, testCase "0_1 is a symbol" $
parseSexp' "0_1"
`sexpEq` Right (Symbol "0_1")
, testCase "1e2 is a symbol" $
parseSexp' "1e2"
`sexpEq` Right (Symbol "1e2")
, testCase "-1e2 is a symbol" $
parseSexp' "-1e2"
`sexpEq` Right (Symbol "-1e2")
, testCase "comments" $
parseSexp' ";; hello, world\n 123"
`sexpEq` Right (Number 123)
, testCase "cyrillic characters in comments" $
parseSexp' ";; Я в серці маю те, що не вмирає!\n SS17"
`sexpEq` Right (Symbol "SS17")
, testCase "unicode math in comments" $
parseSexp' ";; Γ σ ⊢ → ∘ ℕ ∑ ∏ ẽ ∀\nfoobar"
`sexpEq` Right (Symbol "foobar")
, testCase "hello-world is symbol" $
parseSexp' "hello-world"
`sexpEq` Right (Symbol "hello-world")
, testCase "\\forall is a symbol" $
parseSexp' "∀"
`sexpEq` Right (Symbol "∀")
, testCase "\\Bbb{N} is a symbol" $
parseSexp' "ℕ"
`sexpEq` Right (Symbol "ℕ")
, testCase "whitespace and symbol" $
parseSexp' "\t\n hello-world\n"
`sexpEq` Right (Symbol "hello-world")
, testCase "cyrillic characters symbol" $
parseSexp' "символ"
`sexpEq` Right (Symbol "символ")
, testCase "greek characters symbol" $
parseSexp' "αβγΠΣΩ"
`sexpEq` Right (Symbol "αβγΠΣΩ")
, testCase "special-characters \"\\:$%^&*,\" symbol" $
parseSexp' "\\:$%^&*,"
`sexpEq` Right (Symbol "\\:$%^&*,")
, testCase "string with arabic characters" $
parseSexp' "\"ي الخاطفة الجديدة، مع, بلديهم\""
`sexpEq` Right (String "ي الخاطفة الجديدة، مع, بلديهم")
, testCase "string with japanese characters" $
parseSexp' "\"媯綩 づ竤バ り姥娩ぎょひ\""
`sexpEq` Right (String "媯綩 づ竤バ り姥娩ぎょひ")
, testCase "string with newline" $
parseSexp' "\"foo\nbar\""
`sexpEq` Right (String "foo\nbar")
, testCase "string with \\n" $
parseSexp' "\"foo\\nbar\""
`sexpEq` Right (String "foo\nbar")
, testCase "string with \\t" $
parseSexp' "\"foo\\tbar\""
`sexpEq` Right (String "foo\tbar")
, testCase "string with \\\"" $
parseSexp' "\"foo\\\"bar\""
`sexpEq` Right (String "foo\"bar")
, testCase "string with \\\\" $
parseSexp' "\"foo\\\\bar\""
`sexpEq` Right (String "foo\\bar")
, testCase "paren-list" $
parseSexp' "(foo bar)"
`sexpEq` Right (ParenList [Symbol "foo", Symbol "bar"])
, testCase "bracket-list" $
parseSexp' "[foo bar]"
`sexpEq` Right (BracketList [Symbol "foo", Symbol "bar"])
, testCase "brace-list" $
parseSexp' "{foo bar}"
`sexpEq` Right (BraceList [Symbol "foo", Symbol "bar"])
, testCase "quoted" $
parseSexp' "'foo"
`sexpEq` Right (Modified Quote (Symbol "foo"))
, testCase "hashed" $
parseSexp' "#foo"
`sexpEq` Right (Modified Hash (Symbol "foo"))
, testCase "keyword" $
parseSexp' ":foo"
`sexpEq` Right (Symbol ":foo")
]
grammarTests :: TestTree
grammarTests = testGroup "Grammar tests"
[ baseTypeTests
, listTests
, prefixTests
, dictTests
, revStackPrismTests
, parseTests
, genTests
, parseGenTests
]
baseTypeTests :: TestTree
baseTypeTests = testGroup "Base type combinator tests"
[ testCase "bool/true" $
fromSexp' sexpIso (Symbol "true") `otherEq`
Right True
, testCase "bool/false" $
fromSexp' sexpIso (Symbol "false") `otherEq`
Right False
, testCase "integer" $
fromSexp' integer (Number (42 ^ (42 :: Integer))) `otherEq`
Right (42 ^ (42 :: Integer))
, testCase "int" $
fromSexp' int (Number 65536) `otherEq`
Right 65536
, testCase "real" $
fromSexp' real (Number 3.14) `otherEq`
Right 3.14
, testCase "double" $
fromSexp' double (Number 3.14) `otherEq`
Right 3.14
, testCase "string" $
fromSexp' string (String "foo\nbar baz") `otherEq`
Right "foo\nbar baz"
, testCase "string'" $
fromSexp' string' (String "foo\nbar baz") `otherEq`
Right "foo\nbar baz"
, testCase "symbol" $
fromSexp' symbol (Symbol "foobarbaz") `otherEq`
Right "foobarbaz"
]
listTests :: TestTree
listTests = testGroup "List combinator tests"
[ testCase "empty list of ints" $
fromSexp'
(list (rest int))
(ParenList []) `otherEq`
Right []
, testCase "list of strings" $
fromSexp'
(list (rest string))
(ParenList [String "tt", String "ff", String "ff"]) `otherEq`
Right ["tt", "ff", "ff"]
, testCase "bracket list of ints" $
fromSexp'
(bracketList (rest int))
(BracketList [Number 123, Number 0, Number (-100)]) `otherEq`
Right [123, 0, -100]
, testCase "brace list of strings" $
fromSexp'
(braceList (rest string))
(BraceList [String "foo", String "bar"]) `otherEq`
Right ["foo", "bar"]
]
dictTests :: TestTree
dictTests = testGroup "Dict combinator tests"
[ testCase "simple dict, present key" $
fromSexp'
(braceList (props (key "foo" int)))
(BraceList [Symbol ":foo", Number 42]) `otherEq`
Right 42
, testCase "simple dict, missing key" $
fromSexp'
(braceList (props (key "bar" int)))
(BraceList [Symbol ":foo", Number 42]) `otherEq`
(Left (ErrorMessage dummyPos [] (S.fromList ["keyword :bar"]) Nothing))
, testCase "simple dict, missing optional key" $
fromSexp'
(braceList (props (optKey "bar" int)))
(BraceList []) `otherEq`
Right Nothing
, testCase "simple dict, extra key" $
fromSexp'
(braceList (props (key "foo" int)))
(BraceList [Symbol ":foo", Number 42, Symbol ":bar", Number 0]) `otherEq`
(Left (ErrorMessage dummyPos [] mempty (Just "keyword :bar")))
, testCase "simple dict, remaining keys, from" $
fromSexp'
(braceList (props (restKeys (int >>> pair))))
(BraceList [Symbol ":foo", Number 42, Symbol ":bar", Number 0]) `otherEq`
(Right [("foo", 42), ("bar", 0)])
, testCase "simple dict, remaining keys, to" $
toSexp'
(braceList (props (restKeys (int >>> pair))))
[("foo", 42), ("bar", 0)] `sexpEq`
(Right (BraceList [Symbol ":foo", Number 42, Symbol ":bar", Number 0]))
, testCase "simple dict, remaining keys then one more" $
fromSexp'
(braceList (props (restKeys (int >>> pair) >>> key "baz" int)) >>> pair)
(BraceList [Symbol ":foo", Number 42, Symbol ":bar", Number 0]) `otherEq`
(Left (ErrorMessage dummyPos [] (S.fromList ["keyword :baz"]) Nothing))
]
prefixTests :: TestTree
prefixTests = testGroup "Prefix combinator tests"
[ testCase "quoted" $
fromSexp'
(quoted (list (rest int)))
(Modified Quote (ParenList [Number 1, Number 2])) `otherEq`
Right [1, 2]
, testCase "hashed" $
fromSexp'
(hashed (bracketList (rest int)))
(Modified Hash (BracketList [Number 1, Number 2])) `otherEq`
Right [1, 2]
, testCase "backticked" $
fromSexp'
(prefixed Backtick (bracketList (rest int)))
(Modified Backtick (BracketList [Number 123, Number 0, Number (-100)])) `otherEq`
Right [123, 0, -100]
, testCase "comma-ed" $
fromSexp'
(prefixed Comma (bracketList (rest int)))
(Modified Comma (BracketList [Number 123, Number 0, Number (-100)])) `otherEq`
Right [123, 0, -100]
, testCase "comma-at-ed" $
fromSexp'
(prefixed CommaAt (bracketList (rest int)))
(Modified CommaAt (BracketList [Number 123, Number 0, Number (-100)])) `otherEq`
Right [123, 0, -100]
]
revStackPrismTests :: TestTree
revStackPrismTests = testGroup "Reverse stack prism tests"
[ testCase "pair of two bools" $
fromSexp' sexpIso (ParenList [Symbol "false", Symbol "true"]) `otherEq`
Right (Pair False True)
, testCase "sum of products (Bar True 42)" $
fromSexp' sexpIso (ParenList [Symbol "bar", Symbol "true", Number 42]) `otherEq`
Right (Bar True (42 :: Int))
, testCase "sum of products (Baz True False) tries to parse (baz #f 10)" $
fromSexp' (sexpIso :: SexpGrammar (Foo Bool Bool))
(ParenList [Symbol "baz", Symbol "false", Number 10]) `otherEq`
(Left (ErrorMessage dummyPos [] (S.fromList ["symbol false", "symbol true"]) (Just "10")))
]
testArithExpr :: ArithExpr
testArithExpr =
Add (Lit 0) (Mul [])
testArithExprSexp :: Sexp
testArithExprSexp =
ParenList [Symbol "+", Number 0, ParenList [Symbol "*"]]
parseTests :: TestTree
parseTests = testGroup "parse tests"
[ testCase "(+ 0 (*))" $
fromSexp' arithExprGenericIso testArithExprSexp
`otherEq` Right testArithExpr
]
genTests :: TestTree
genTests = testGroup "gen tests"
[ testCase "(+ 0 (*))" $
toSexp' arithExprGenericIso testArithExpr
`otherEq` Right testArithExprSexp
]
genParseIdentityProp :: forall a. (Eq a) => (forall t. Grammar Position (Sexp :- t) (a :- t)) -> a -> Bool
genParseIdentityProp iso expr =
(toSexp' iso expr >>= fromSexp' iso :: Either (ErrorMessage Position) a)
==
Right expr
parseGenTests :: TestTree
parseGenTests = testGroup "parse . gen == id"
[ QC.testProperty "ArithExprs/TH" $
genParseIdentityProp arithExprTHIso
, QC.testProperty "ArithExprs/Generics" $
genParseIdentityProp arithExprGenericIso
, QC.testProperty "Pair Int String" $
genParseIdentityProp (pairGenericIso int string')
, QC.testProperty "Foo (Foo Int String) (Pair String Int)" $
genParseIdentityProp (fooGenericIso (fooGenericIso int string') (pairGenericIso string' int))
, QC.testProperty "Person" $
genParseIdentityProp personGenericIso
]
main :: IO ()
main = defaultMain allTests