sexp-grammar 1.2.1 → 1.2.2
raw patch · 13 files changed
+211/−110 lines, 13 files
Files
- README.md +7/−5
- bench/Main.hs +4/−4
- sexp-grammar.cabal +4/−3
- src/Language/Sexp.hs +6/−6
- src/Language/Sexp/Lexer.x +52/−15
- src/Language/Sexp/LexerInterface.hs +86/−0
- src/Language/Sexp/Pretty.hs +6/−6
- src/Language/Sexp/Types.hs +0/−3
- src/Language/SexpGrammar.hs +7/−6
- src/Language/SexpGrammar/Base.hs +14/−7
- src/Language/SexpGrammar/Combinators.hs +6/−0
- src/Language/SexpGrammar/Parser.hs +0/−53
- test/Main.hs +19/−2
README.md view
@@ -7,7 +7,9 @@ into S-expressions. Just write a grammar once and get both parser and pretty-printer, for free. -The package is heavily inspired by the paper+**WARNING: highly unstable and experimental software. Not intended for production**++The approach used in `sexp-grammar` is inspired by the paper [Invertible syntax descriptions: Unifying parsing and pretty printing] (http://www.informatik.uni-marburg.de/~rendel/unparse/) and a similar implementation of invertible grammar approach for JSON, library by Martijn van@@ -26,7 +28,7 @@ } deriving (Show, Generic) personGrammar :: SexpG Person-personGrammar = with $ -- Person is isomorphic to+personGrammar = with $ -- Person is isomorphic to: list ( -- a list with el (sym "person") >>> -- a symbol "person", el string' >>> -- a string, and@@ -35,8 +37,8 @@ Kw "age" .:? int)) -- an optional keyword :age with int value. ``` -So now we can use `personGrammar` to parse S-expressions to `Person`-record and pretty-print records of `Person` type back to S-expression:+So now we can use `personGrammar` to parse S-expressions to records of type+`Person` and pretty-print records of type `Person` back to S-expressions: ```haskell ghci> import Language.SexpGrammar@@ -44,7 +46,7 @@ ghci> person <- either error id . decodeWith personGrammar . B8.pack <$> getLine (person "John Doe" :address "42 Whatever str." :age 25) ghci> person-Right (Person {pName = "John Doe", pAddress = "42 Whatever str.", pAge = Just 25})+Person {pName = "John Doe", pAddress = "42 Whatever str.", pAge = Just 25} ghci> either print B8.putStrLn . encodeWith personGrammar $ person (person "John Doe" :address "42 Whatever str." :age 25) ```
bench/Main.hs view
@@ -9,8 +9,8 @@ import Control.Arrow import Control.Category-import qualified Data.ByteString.Lazy.Char8 as B8 import Data.Data (Data, Typeable)+import qualified Data.Text.Lazy as TL import qualified Language.Sexp as Sexp import Language.SexpGrammar import Language.SexpGrammar.TH@@ -69,11 +69,11 @@ genExpr :: Expr -> Either String Sexp genExpr = genSexp sexpIso -expr :: B8.ByteString -> Expr+expr :: TL.Text -> Expr expr = either error id . decode -benchCases :: [(String, B8.ByteString)]-benchCases = map (\a -> ("expression " ++ take 40 (B8.unpack a) ++ "...", a))+benchCases :: [(String, TL.Text)]+benchCases = map (\a -> ("expression " ++ take 40 (TL.unpack a) ++ "...", a)) [ "(+ 1 20)" , "(+ (+ 2 20) 0)" , "(+ (+ 3 20) (+ 10 20))"
sexp-grammar.cabal view
@@ -1,5 +1,5 @@ name: sexp-grammar-version: 1.2.1+version: 1.2.2 license: BSD3 license-file: LICENSE author: Eugene Smolanka, Sergey Vinokurov@@ -40,6 +40,7 @@ Data.InvertibleGrammar.Generic Data.InvertibleGrammar.TH Control.Monad.ContextError+ Language.Sexp.LexerInterface Language.Sexp.Lexer Language.Sexp.Parser Language.Sexp.Token@@ -47,7 +48,6 @@ Language.SexpGrammar.Base Language.SexpGrammar.Class Language.SexpGrammar.Combinators- Language.SexpGrammar.Parser build-depends: array@@ -72,13 +72,13 @@ build-depends: QuickCheck , base- , bytestring , scientific , semigroups , sexp-grammar , tasty , tasty-hunit , tasty-quickcheck+ , text main-is: Main.hs hs-source-dirs: test default-language: Haskell2010@@ -92,6 +92,7 @@ , scientific , semigroups , sexp-grammar+ , text main-is: Main.hs hs-source-dirs: bench default-language: Haskell2010
src/Language/Sexp.hs view
@@ -20,7 +20,7 @@ , getPos ) where -import qualified Data.ByteString.Lazy.Char8 as B8+import qualified Data.Text.Lazy as TL import Language.Sexp.Types import Language.Sexp.Parser (parseSexp_, parseSexps_)@@ -29,25 +29,25 @@ import Language.Sexp.Encode (encode) -- | Quickly decode a ByteString-formatted S-expression into Sexp structure-decode :: B8.ByteString -> Either String Sexp+decode :: TL.Text -> Either String Sexp decode = parseSexp "<str>" -- | Parse a ByteString-formatted S-expression into Sexp -- structure. Takes file name for better error messages.-parseSexp :: FilePath -> B8.ByteString -> Either String Sexp+parseSexp :: FilePath -> TL.Text -> Either String Sexp parseSexp fn inp = parseSexp_ (lexSexp (Position fn 1 0) inp) -- | Parse a ByteString-formatted sequence of S-expressions into list -- of Sexp structures. Takes file name for better error messages.-parseSexps :: FilePath -> B8.ByteString -> Either String [Sexp]+parseSexps :: FilePath -> TL.Text -> Either String [Sexp] parseSexps fn inp = parseSexps_ (lexSexp (Position fn 1 0) inp) -- | Parse a ByteString-formatted S-expression into Sexp -- structure. Takes file name for better error messages.-parseSexp' :: Position -> B8.ByteString -> Either String Sexp+parseSexp' :: Position -> TL.Text -> Either String Sexp parseSexp' pos inp = parseSexp_ (lexSexp pos inp) -- | Parse a ByteString-formatted sequence of S-expressions into list -- of Sexp structures. Takes file name for better error messages.-parseSexps' :: Position -> B8.ByteString -> Either String [Sexp]+parseSexps' :: Position -> TL.Text -> Either String [Sexp] parseSexps' pos inp = parseSexps_ (lexSexp pos inp)
src/Language/Sexp/Lexer.x view
@@ -1,5 +1,6 @@ {-{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE NamedFieldPuns #-} {-# OPTIONS_GHC -fno-warn-missing-signatures #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} {-# OPTIONS_GHC -fno-warn-tabs #-}@@ -16,19 +17,26 @@ import qualified Data.Text.Lazy as TL import Data.Text.Lazy.Encoding (decodeUtf8) import qualified Data.ByteString.Lazy.Char8 as B8++import Language.Sexp.LexerInterface import Language.Sexp.Token import Language.Sexp.Types (Position (..))-} -%wrapper "posn-bytestring"+} $whitechar = [\ \t\n\r\f\v]+$unispace = \x01+$whitespace = [$whitechar $unispace] +$uninonspace = \x02+$uniany = [$unispace $uninonspace]+@any = (. | $uniany)+ $digit = 0-9 $hex = [0-9 A-F a-f] $alpha = [a-z A-Z] -$graphic = [$alpha $digit \!\#\$\%\&\*\+\.\/\<\=\>\?\@\\\^\|\-\~ \(\)\,\;\[\]\`\{\} \:\"\'\_]+$graphic = [$alpha $digit \!\#\$\%\&\*\+\.\/\<\=\>\?\@\\\^\|\-\~ \(\)\,\;\[\]\`\{\} \:\"\'\_ $uninonspace] @intnum = [\-\+]? $digit+ @scinum = [\-\+]? $digit+ ([\.]$digit+)? ([eE] [\-\+]? $digit+)?@@ -37,15 +45,15 @@ @escape = \\ ($charesc | $digit+ | x $hex+) @string = $graphic # [\"\\] | " " | @escape -$idinitial = [$alpha \!\$\%\&\*\/\<\=\>\?\~\_\^\.\+\-]-$idsubseq = [$idinitial $digit \:]+$idinitial = [$alpha \!\$\%\&\*\/\<\=\>\?\~\_\^\.\+\- $uninonspace]+$idsubseq = [$idinitial $digit \: $uninonspace] @identifier = $idinitial $idsubseq* @keyword = ":" $idsubseq+ :- -$whitechar+ ;-";".* ;+$whitespace+ ;+";" @any* ; "(" { just TokLParen } ")" { just TokRParen } "[" { just TokLBracket }@@ -63,6 +71,8 @@ { +type AlexAction = LineCol -> TL.Text -> LocatedBy LineCol Token+ readInteger :: T.Text -> Integer readInteger str = case signed decimal str of@@ -75,18 +85,45 @@ readString = T.pack . read . T.unpack -just :: Token -> AlexPosn -> B8.ByteString -> LocatedBy AlexPosn Token+just :: Token -> AlexAction just tok pos _ = L pos tok -via :: (a -> Token) -> (T.Text -> a) -> AlexPosn -> B8.ByteString -> LocatedBy AlexPosn Token+via :: (a -> Token) -> (T.Text -> a) -> AlexAction via ftok f pos str =- L pos . ftok . f . TL.toStrict . decodeUtf8 $ str+ L pos . ftok . f . TL.toStrict $str -lexSexp :: Position -> B8.ByteString -> [LocatedBy Position Token]-lexSexp (Position fn line1 col1) = map (mapPosition fixPos) . alexScanTokens+alexScanTokens :: AlexInput -> [LocatedBy LineCol Token]+alexScanTokens input =+ case alexScan input defaultCode of+ AlexEOF -> []+ AlexError (AlexInput {aiInput, aiLineCol = LineCol line col}) ->+ error $ "Lexical error at line " ++ show line ++ " column " ++ show col +++ ". Remaining input: " ++ TL.unpack (TL.take 1000 aiInput)+ AlexSkip input _ -> alexScanTokens input+ AlexToken input' tokLen action ->+ action (aiLineCol input) inputText : alexScanTokens input'+ where+ -- It is safe to take token length from input because every byte Alex+ -- sees corresponds to exactly one character, even if character is a+ -- Unicode one that occupies several bytes. We do character translation+ -- in LexerInterface.alexGetByte function so that all unicode characters+ -- occupy single byte.+ --+ -- On the other hand, taking N characters from Text will take N valid+ -- characters, not N bytes.+ --+ -- Thus, we're good.+ inputText = TL.take (fromIntegral tokLen) $ aiInput input where- fixPos (AlexPn _ l c) | l == 1 = Position fn line1 (col1 + c)- | otherwise = Position fn (pred l + line1) c+ defaultCode :: Int+ defaultCode = 0++lexSexp :: Position -> TL.Text -> [LocatedBy Position Token]+lexSexp (Position fn line1 col1) =+ map (mapPosition fixPos) . alexScanTokens . mkAlexInput+ where+ fixPos (LineCol l c) | l == 1 = Position fn line1 (col1 + c)+ | otherwise = Position fn (pred l + line1) c }
+ src/Language/Sexp/LexerInterface.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}++module Language.Sexp.LexerInterface+ ( LineCol(..)+ , AlexInput(..)+ , mkAlexInput+ -- Alex interfare+ , alexInputPrevChar+ , alexGetByte+ ) where++import Control.Applicative ((<|>))+import Data.Char+import Data.Maybe+import qualified Data.Text.Lazy as TL+import Data.Word (Word8)++data LineCol = LineCol {-# UNPACK #-} !Int {-# UNPACK #-} !Int++columnsInTab :: Int+columnsInTab = 8++advanceLineCol :: Char -> LineCol -> LineCol+advanceLineCol '\n' (LineCol line _) = LineCol (line + 1) 0+advanceLineCol '\t' (LineCol line col) = LineCol line (col + columnsInTab)+advanceLineCol _ (LineCol line col) = LineCol line (col + 1)++data AlexInput = AlexInput+ { aiInput :: TL.Text+ , aiPrevChar :: {-# UNPACK #-} !Char+ , aiLineCol :: !LineCol+ }++mkAlexInput :: TL.Text -> AlexInput+mkAlexInput source = AlexInput+ { aiInput = stripBOM source+ , aiPrevChar = '\n'+ , aiLineCol = initPos+ }+ where+ initPos :: LineCol+ initPos = LineCol 1 0+ stripBOM :: TL.Text -> TL.Text+ stripBOM xs =+ fromMaybe xs $+ TL.stripPrefix utf8BOM xs <|> TL.stripPrefix utf8BOM' xs+ utf8BOM = "\xFFEF"+ utf8BOM' = "\xFEFF"++-- Alex interface - functions usedby Alex+alexInputPrevChar :: AlexInput -> Char+alexInputPrevChar = aiPrevChar++alexGetByte :: AlexInput -> Maybe (Word8, AlexInput)+alexGetByte input@AlexInput {aiInput, aiLineCol} =+ case TL.uncons aiInput of+ Nothing -> Nothing+ Just (c, cs) -> Just $ encode c cs+ where+ encode :: Char -> TL.Text -> (Word8, AlexInput)+ encode c cs = (b, input')+ where+ b :: Word8+ b = fromIntegral $ ord $ fixChar c+ input' :: AlexInput+ input' = input+ { aiInput = cs+ , aiPrevChar = c+ , aiLineCol = advanceLineCol c aiLineCol+ }++-- Translate unicode character into special symbol we taught Alex to recognize.+fixChar :: Char -> Char+fixChar c+ -- Plain ascii case+ | c <= '\x7f' = c+ -- Unicode caset+ | otherwise+ = case generalCategory c of+ Space -> space+ _ -> nonSpaceUnicode+ where+ space = '\x01'+ nonSpaceUnicode = '\x02'
src/Language/Sexp/Pretty.hs view
@@ -52,13 +52,13 @@ pretty = ppSexp -- | Pretty-print a Sexp to a Text-prettySexp' :: Sexp -> Lazy.Text-prettySexp' = displayT . renderPretty 0.75 79 . ppSexp+prettySexp :: Sexp -> Lazy.Text+prettySexp = displayT . renderPretty 0.75 79 . ppSexp -- | Pretty-print a Sexp to a ByteString-prettySexp :: Sexp -> ByteString-prettySexp = encodeUtf8 . prettySexp'+prettySexp' :: Sexp -> ByteString+prettySexp' = encodeUtf8 . prettySexp -- | Pretty-print a list of Sexps as a sequence of S-expressions to a ByteString-prettySexps :: [Sexp] -> ByteString-prettySexps = encodeUtf8 . displayT . renderPretty 0.75 79 . vcat . punctuate (line <> line) . map ppSexp+prettySexps :: [Sexp] -> Lazy.Text+prettySexps = displayT . renderPretty 0.75 79 . vcat . punctuate (line <> line) . map ppSexp
src/Language/Sexp/Types.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE DeriveFoldable #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE FlexibleInstances #-} module Language.Sexp.Types
src/Language/SexpGrammar.hs view
@@ -102,6 +102,7 @@ ) where import Data.ByteString.Lazy.Char8 (ByteString)+import qualified Data.Text.Lazy as TL import Data.InvertibleGrammar import Data.InvertibleGrammar.Monad @@ -132,12 +133,12 @@ -- | Deserialize a value from a lazy 'ByteString'. The input must -- contain exactly one S-expression. Comments are ignored.-decode :: SexpIso a => ByteString -> Either String a+decode :: SexpIso a => TL.Text -> Either String a decode = decodeWith sexpIso -- | Like 'decode' but uses specified grammar.-decodeWith :: SexpG a -> ByteString -> Either String a+decodeWith :: SexpG a -> TL.Text -> Either String a decodeWith g input = Sexp.decode input >>= parseSexp g @@ -159,21 +160,21 @@ -- one S-expression. Unlike 'decode' it takes an additional argument -- with a file name which is being parsed. It is used for error -- messages.-decodeNamed :: SexpIso a => FilePath -> ByteString -> Either String a+decodeNamed :: SexpIso a => FilePath -> TL.Text -> Either String a decodeNamed fn = decodeNamedWith sexpIso fn -- | Like 'decodeNamed' but uses specified grammar.-decodeNamedWith :: SexpG a -> FilePath -> ByteString -> Either String a+decodeNamedWith :: SexpG a -> FilePath -> TL.Text -> Either String a decodeNamedWith g fn input = Sexp.parseSexp fn input >>= parseSexp g -- | Pretty-prints a value serialized to a lazy 'ByteString'.-encodePretty :: SexpIso a => a -> Either String ByteString+encodePretty :: SexpIso a => a -> Either String TL.Text encodePretty = encodePrettyWith sexpIso -- | Like 'encodePretty' but uses specified grammar.-encodePrettyWith :: SexpG a -> a -> Either String ByteString+encodePrettyWith :: SexpG a -> a -> Either String TL.Text encodePrettyWith g = fmap Sexp.prettySexp . genSexp g
src/Language/SexpGrammar/Base.hs view
@@ -36,7 +36,7 @@ import Data.InvertibleGrammar import Data.InvertibleGrammar.Monad-import Language.Sexp.Pretty (prettySexp')+import Language.Sexp.Pretty (prettySexp) import Language.Sexp.Types -- | Grammar which matches Sexp to a value of type a and vice versa.@@ -51,11 +51,11 @@ unexpectedSexp :: (MonadContextError (Propagation Position) (GrammarError Position) m) => Text -> Sexp -> m a unexpectedSexp exp got =- grammarError $ expected exp `mappend` unexpected (Lazy.toStrict $ prettySexp' got)+ grammarError $ expected exp `mappend` unexpected (Lazy.toStrict $ prettySexp got) unexpectedAtom :: (MonadContextError (Propagation Position) (GrammarError Position) m) => Atom -> Atom -> m a unexpectedAtom expected atom = do- unexpectedSexp (Lazy.toStrict $ prettySexp' (Atom dummyPos expected)) (Atom dummyPos atom)+ unexpectedSexp (Lazy.toStrict $ prettySexp (Atom dummyPos expected)) (Atom dummyPos atom) unexpectedAtomType :: (MonadContextError (Propagation Position) (GrammarError Position) m) => Text-> Atom -> m a unexpectedAtomType expected atom = do@@ -66,6 +66,7 @@ -- Top-level grammar data SexpGrammar a b where+ GPos :: SexpGrammar (Sexp :- t) (Position :- Sexp :- t) GAtom :: Grammar AtomGrammar (Atom :- t) t' -> SexpGrammar (Sexp :- t) t' GList :: Grammar SeqGrammar t t' -> SexpGrammar (Sexp :- t) t' GVect :: Grammar SeqGrammar t t' -> SexpGrammar (Sexp :- t) t'@@ -74,6 +75,9 @@ ( MonadPlus m , MonadContextError (Propagation Position) (GrammarError Position) m ) => InvertibleGrammar m SexpGrammar where+ forward GPos (s :- t) =+ return (getPos s :- s :- t)+ forward (GAtom g) (s :- t) = case s of Atom p a -> dive $ locate p >> forward g (a :- t)@@ -89,6 +93,9 @@ Vector p xs -> dive $ locate p >> parseSequence xs g t other -> locate (getPos other) >> unexpectedSexp "vector" other + backward GPos (_ :- s :- t) =+ return (s :- t)+ backward (GAtom g) t = do (a :- t') <- dive $ backward g t return (Atom dummyPos a :- t')@@ -177,7 +184,7 @@ (a, SeqCtx rest) <- runStateT (forward g t) (SeqCtx xs) unless (null rest) $ unexpectedStr $ "leftover elements: " `mappend`- (Lazy.toStrict $ Lazy.unwords $ map prettySexp' rest)+ (Lazy.toStrict $ Lazy.unwords $ map prettySexp rest) return a data SeqGrammar a b where@@ -226,14 +233,14 @@ when (not $ M.null ctx) $ unexpectedStr $ "property-list keys: " `mappend` (Lazy.toStrict $ Lazy.unwords $- map (prettySexp' . Atom dummyPos . AtomKeyword) (M.keys ctx))+ map (prettySexp . Atom dummyPos . AtomKeyword) (M.keys ctx)) return res where go [] props = return props go (Atom _ (AtomKeyword kwd):x:xs) props = step >> go xs (M.insert kwd x props) go other _ = unexpectedStr $ "malformed property-list: " `mappend`- (Lazy.toStrict $ Lazy.unwords $ map prettySexp' other)+ (Lazy.toStrict $ Lazy.unwords $ map prettySexp other) backward (GElem g) t = do step@@ -284,7 +291,7 @@ case M.lookup kwd ps of Nothing -> unexpectedStr $ mconcat [ "key "- , Lazy.toStrict . prettySexp' . Atom dummyPos . AtomKeyword $ kwd+ , Lazy.toStrict . prettySexp . Atom dummyPos . AtomKeyword $ kwd , " not found" ] Just x -> do
src/Language/SexpGrammar/Combinators.hs view
@@ -30,6 +30,7 @@ , (.:) , (.:?) -- ** Utility grammars+ , position , pair , unpair , swap@@ -146,6 +147,11 @@ -- | Define a grammar for a constant keyword kw :: Kw -> SexpG_ kw = Inject . GAtom . Inject . GKw++-- | Get position of Sexp. Doesn't consume Sexp and doesn't have any+-- effect on backward run.+position :: Grammar SexpGrammar (Sexp :- t) (Position :- Sexp :- t)+position = Inject GPos ---------------------------------------------------------------------- -- Special combinators
− src/Language/SexpGrammar/Parser.hs
@@ -1,53 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}--module Language.SexpGrammar.Parser where--import Control.Applicative-#if MIN_VERSION_mtl(2, 2, 0)-import Control.Monad.Except-#else-import Control.Monad.Error-#endif--data Result a- = Success a- | Failure String- deriving (Functor)--instance Applicative Result where- pure = Success- Success f <*> Success a = Success (f a)- Failure a <*> Success _ = Failure a- Success _ <*> Failure b = Failure b- Failure a <*> Failure b = Failure $ a ++ "\n" ++ b--instance Monad Result where- return = Success- Failure a >>= _ = Failure a- Success a >>= f = f a--instance Alternative Result where- empty = Failure "empty"- Success a <|> _ = Success a- Failure _ <|> Success b = Success b- Failure a <|> Failure b = Failure (a ++ "\n" ++ b)--instance MonadPlus Result where- mzero = empty- mplus = (<|>)--instance MonadError [Char] Result where- throwError = Failure- catchError res handle =- case res of- Success a -> Success a- Failure b -> handle b--runR :: (a -> Result b) -> a -> Either String b-runR parser a =- case parser a of- Success a -> Right a- Failure b -> Left $ "List of failures:\n" ++ b
test/Main.hs view
@@ -19,7 +19,7 @@ #endif import Control.Category-import qualified Data.ByteString.Lazy.Char8 as B8+import qualified Data.Text.Lazy as TL import Data.Scientific import Data.Semigroup import Test.QuickCheck ()@@ -48,7 +48,7 @@ stripPos (Quoted _ x) = Quoted dummyPos $ stripPos x parseSexp' :: String -> Either String Sexp-parseSexp' input = stripPos <$> Sexp.decode (B8.pack input)+parseSexp' input = stripPos <$> Sexp.decode (TL.pack input) data Pair a b = Pair a b deriving (Show, Eq, Ord, Generic)@@ -145,6 +145,23 @@ parseSexp' "-123" @?= Right (Int' (- 123)) , testCase "+123.4e5 is a floating number" $ parseSexp' "+123.4e5" @?= Right (Real' (read "+123.4e5" :: Scientific))+ , testCase "comments" $+ parseSexp' ";; hello, world\n 123" @?= Right (Int' 123)+ , testCase "cyrillic characters in comments" $+ parseSexp' ";; привет!\n 123" @?= Right (Int' 123)+ , testCase "unicode math in comments" $+ parseSexp' ";; Γ ctx\n;; ----- Nat-formation\n;; Γ ⊦ Nat : Type\nfoobar" @?=+ Right (Symbol' "foobar")+ , testCase "symbol" $+ parseSexp' "hello-world" @?= Right (Symbol' "hello-world")+ , testCase "cyrillic symbol" $+ parseSexp' "привет-мир" @?= Right (Symbol' "привет-мир")+ , testCase "string with arabic characters" $+ parseSexp' "\"ي الخاطفة الجديدة، مع, بلديهم\"" @?=+ Right (String' "ي الخاطفة الجديدة، مع, بلديهم")+ , testCase "string with japanese characters" $+ parseSexp' "\"媯綩 づ竤バ り姥娩ぎょひ\"" @?=+ Right (String' "媯綩 づ竤バ り姥娩ぎょひ") ] grammarTests :: TestTree