packages feed

sexp-grammar 1.2.1 → 1.2.2

raw patch · 13 files changed

+211/−110 lines, 13 files

Files

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