sexp-grammar 2.0.0 → 2.0.1
raw patch · 8 files changed
+162/−70 lines, 8 filesdep −split
Dependencies removed: split
Files
- sexp-grammar.cabal +1/−2
- src/Language/Sexp/Encode.hs +3/−19
- src/Language/Sexp/Lexer.x +3/−20
- src/Language/Sexp/Pretty.hs +4/−3
- src/Language/Sexp/Token.hs +54/−0
- src/Language/Sexp/Types.hs +3/−4
- src/Language/SexpGrammar/Base.hs +18/−17
- test/Main.hs +76/−5
sexp-grammar.cabal view
@@ -1,5 +1,5 @@ name: sexp-grammar-version: 2.0.0+version: 2.0.1 license: BSD3 license-file: LICENSE author: Eugene Smolanka, Sergey Vinokurov@@ -58,7 +58,6 @@ , recursion-schemes >=5.0 && <6.0 , scientific >=0.3.3 && <0.4 , semigroups >=0.16 && <0.19- , split >=0.2 && <0.3 , text >=1.2 && <1.3 , utf8-string >=1.0 && <2.0
src/Language/Sexp/Encode.hs view
@@ -5,15 +5,11 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} -module Language.Sexp.Encode- ( encode- , escape- ) where+module Language.Sexp.Encode (encode) where import Data.Functor.Foldable (cata) import Data.List (intersperse) import Data.Scientific-import qualified Data.Text as T import qualified Data.Text.Encoding as T (encodeUtf8) import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL (encodeUtf8)@@ -25,19 +21,7 @@ #endif import Language.Sexp.Types--escape :: T.Text -> TL.Text-escape = TL.concat . go [] . TL.fromStrict- where- go acc text- | TL.null text = acc- | otherwise =- let (chunk, rest) = TL.break (\c -> c == '"' || c == '\\') text- in case TL.uncons rest of- Nothing -> chunk : acc- Just ('"', rest') -> go (chunk : "\\\"" : acc) rest'- Just ('\\',rest') -> go (chunk : "\\\\" : acc) rest'- Just (other, rest') -> go (chunk : TL.singleton other : acc) rest'+import Language.Sexp.Token (escape) buildSexp :: Fix SexpF -> Builder buildSexp = cata alg@@ -51,7 +35,7 @@ AtomNumber a | isInteger a -> string8 (formatScientific Fixed (Just 0) a) | otherwise -> string8 (formatScientific Fixed Nothing a)- AtomString a -> char8 '"' <> lazyByteString (TL.encodeUtf8 (escape a)) <> char8 '"'+ AtomString a -> char8 '"' <> lazyByteString (TL.encodeUtf8 (escape (TL.fromStrict a))) <> char8 '"' AtomSymbol a -> byteString (T.encodeUtf8 a) ParenListF ss -> char8 '(' <> hsep ss <> char8 ')' BracketListF ss -> char8 '[' <> hsep ss <> char8 ']'
src/Language/Sexp/Lexer.x view
@@ -26,8 +26,6 @@ import Language.Sexp.Token import Language.Sexp.Types (Position (..), LocatedBy (..)) -import Debug.Trace- } $hspace = [\ \t]@@ -46,8 +44,8 @@ $unicode = $allgraphic # [\x20-\x80] -$syminitial = [$alpha \:\@\#\!\$\%\&\*\/\<\=\>\?\~\_\^\.\|\+\- $unicode]-$symsubseq = [$syminitial $digit \'\`\,]+$syminitial = [$alpha \:\@\!\$\%\&\*\/\<\=\>\?\~\_\^\.\|\+\- $unicode]+$symsubseq = [$syminitial $digit \#\'\`\,] @symescape = \\ [$alpha $digit \(\)\[\]\{\}\\\|\;\'\`\"\#\.\,] @symbol = ($syminitial | @symescape) ($symsubseq | @symescape)* @@ -80,22 +78,7 @@ type AlexAction = LineCol -> ByteString -> LocatedBy LineCol Token readString :: ByteString -> T.Text-readString = TL.toStrict . TL.concat . unescape [] . TL.tail . TL.init . decodeUtf8- where- unescape acc text- | TL.null text = acc- | otherwise =- let (chunk, rest) = TL.break (== '\\') text in- case TL.uncons rest of- Nothing -> (chunk : acc)- Just (_, rest') ->- case TL.uncons rest' of- Nothing -> error "Invalid escape sequence"- Just ('n', rest'') -> unescape (chunk `TL.snoc` '\n' : acc) rest''- Just ('r', rest'') -> unescape (chunk `TL.snoc` '\r' : acc) rest''- Just ('t', rest'') -> unescape (chunk `TL.snoc` '\t' : acc) rest''- Just (lit, rest'') -> unescape (chunk `TL.snoc` lit : acc) rest''-+readString = TL.toStrict . unescape . TL.tail . TL.init . decodeUtf8 readNum :: ByteString -> Scientific readNum = read . TL.unpack . decodeUtf8
src/Language/Sexp/Pretty.hs view
@@ -11,21 +11,22 @@ import Data.ByteString.Lazy.Char8 (ByteString) import Data.Functor.Foldable (para) import Data.Scientific+import qualified Data.Text.Lazy as TL import Data.Text.Lazy.Encoding (encodeUtf8) import Data.Text.Prettyprint.Doc+import Data.Text.Prettyprint.Doc.Internal (unsafeTextWithoutNewlines) import qualified Data.Text.Prettyprint.Doc.Render.Text as Render import Language.Sexp.Types-import Language.Sexp.Encode (escape)+import Language.Sexp.Token (escape) instance Pretty Atom where pretty = \case AtomNumber a | isInteger a -> pretty $ formatScientific Fixed (Just 0) a | otherwise -> pretty $ formatScientific Fixed Nothing $ a- AtomString a -> dquotes (pretty (escape a))+ AtomString a -> dquotes (unsafeTextWithoutNewlines . TL.toStrict . escape . TL.fromStrict $ a) AtomSymbol a -> pretty a- ppList :: [(Fix SexpF, Doc ann)] -> Doc ann ppList ls = case ls of
src/Language/Sexp/Token.hs view
@@ -4,11 +4,15 @@ module Language.Sexp.Token ( Token (..) , Prefix (..)+ , escape+ , unescape ) where import Data.Text (Text)+import qualified Data.Text.Lazy as TL import Data.Scientific import Data.Text.Prettyprint.Doc+import Data.Semigroup import Language.Sexp.Types (Prefix(..)) @@ -38,3 +42,53 @@ pretty (TokNumber n) = "number" <+> pretty (show n) pretty (TokString s) = "string" <+> pretty (show s) pretty (TokUnknown u) = "unrecognized" <+> pretty (show u)+++newtype DText = DText (TL.Text -> TL.Text)++instance Semigroup DText where+ DText a <> DText b = DText (a . b)++instance Monoid DText where+ mempty = DText id+ mappend = (<>)++delay :: TL.Text -> DText+delay t = DText (t `TL.append`)++force :: DText -> TL.Text+force (DText f) = f TL.empty+++unescape :: TL.Text -> TL.Text+unescape = force . go mempty+ where+ go :: DText -> TL.Text -> DText+ go acc text+ | TL.null text = acc+ | otherwise =+ let (chunk, rest) = TL.break (== '\\') text in+ case TL.uncons rest of+ Nothing -> acc <> delay chunk+ Just (_, rest') ->+ case TL.uncons rest' of+ Nothing -> error "Invalid escape sequence"+ Just ('n', rest'') -> go (acc <> delay (chunk `TL.snoc` '\n')) rest''+ Just ('r', rest'') -> go (acc <> delay (chunk `TL.snoc` '\r')) rest''+ Just ('t', rest'') -> go (acc <> delay (chunk `TL.snoc` '\t')) rest''+ Just (lit, rest'') -> go (acc <> delay (chunk `TL.snoc` lit )) rest''+++escape :: TL.Text -> TL.Text+escape = force . go mempty+ where+ go :: DText -> TL.Text -> DText+ go acc text+ | TL.null text = acc+ | otherwise =+ let (chunk, rest) = TL.break (\c -> c == '"' || c == '\\') text+ in case TL.uncons rest of+ Nothing -> acc <> delay chunk+ Just ('"', rest') -> go (acc <> delay chunk <> delay "\\\"") rest'+ Just ('\\',rest') -> go (acc <> delay chunk <> delay "\\\\") rest'+ Just (other, rest') -> go (acc <> delay chunk <> delay (TL.singleton other)) rest'
src/Language/Sexp/Types.hs view
@@ -1,10 +1,9 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE DeriveFoldable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} module Language.Sexp.Types ( Atom (..)@@ -117,6 +116,7 @@ go _ _ = False instance NFData Atom+ instance NFData Position instance NFData (Fix SexpF) where@@ -132,4 +132,3 @@ instance NFData (Fix (Compose (LocatedBy Position) SexpF)) where rnf = rnf . stripLocation-
src/Language/SexpGrammar/Base.hs view
@@ -181,7 +181,7 @@ -- -- * @el symbol@ consumes a symbol and produces a 'Text' value -- corresponding to the symbol.-el :: Grammar p (Sexp :- t) t' -> Grammar p (List :- t) (List :- t')+el :: Grammar Position (Sexp :- t) t' -> Grammar Position (List :- t) (List :- t') el g = coerced (Flip cons >>> onTail g >>> Step) @@ -193,8 +193,8 @@ -- >>> encodeWith grammar [2, 3, 5, 7, 11, 13] -- Right "(check-primes 2 3 5 7 11 13)" rest- :: (forall t. Grammar p (Sexp :- t) (a :- t))- -> Grammar p (List :- t) (List :- [a] :- t)+ :: (forall t. Grammar Position (Sexp :- t) (a :- t))+ -> Grammar Position (List :- t) (List :- [a] :- t) rest g = iso coerce coerce >>> onHead (Traverse (sealed g >>> Step)) >>>@@ -203,7 +203,7 @@ ---------------------------------------------------------------------- beginProperties- :: Grammar p (List :- t) (List :- PropertyList :- t)+ :: Grammar Position (List :- t) (List :- PropertyList :- t) beginProperties = Flip $ PartialIso (\(List rest :- PropertyList alist :- t) -> List (concatMap (\(k, v) -> [Atom (AtomSymbol (':' `TS.cons` k)), v]) alist ++ rest) :- t)@@ -219,8 +219,9 @@ takePairs other acc = (other, acc) + endProperties- :: Grammar p t (PropertyList :- t)+ :: Grammar Position t (PropertyList :- t) endProperties = PartialIso (\t -> PropertyList [] :- t) (\(PropertyList lst :- t) ->@@ -247,8 +248,8 @@ -- :} -- Right "{:real 0 :img -1 / :real 1 :img 0}" props- :: Grammar p (PropertyList :- t) (PropertyList :- t')- -> Grammar p (List :- t) (List :- t')+ :: Grammar Position (PropertyList :- t) (PropertyList :- t')+ -> Grammar Position (List :- t) (List :- t') props g = beginProperties >>> Dive (onTail (g >>> Flip endProperties)) @@ -259,8 +260,8 @@ -- Note: performs linear lookup, /O(n)/ key :: Text- -> (forall t. Grammar p (Sexp :- t) (a :- t))- -> Grammar p (PropertyList :- t) (PropertyList :- a :- t)+ -> (forall t. Grammar Position (Sexp :- t) (a :- t))+ -> Grammar Position (PropertyList :- t) (PropertyList :- a :- t) key k g = coerced ( Flip (insert k (expected $ ppKey k)) >>>@@ -275,8 +276,8 @@ -- Note: performs linear lookup, /O(n)/ optKey :: Text- -> (forall t. Grammar p (Sexp :- t) (a :- t))- -> Grammar p (PropertyList :- t) (PropertyList :- Maybe a :- t)+ -> (forall t. Grammar Position (Sexp :- t) (a :- t))+ -> Grammar Position (PropertyList :- t) (PropertyList :- Maybe a :- t) optKey k g = coerced (Flip (insertMay k) >>> Step >>>@@ -290,24 +291,24 @@ -- | Property by a key grammar. Infix version of 'key'. (.:) :: Text- -> (forall t. Grammar p (Sexp :- t) (a :- t))- -> Grammar p (PropertyList :- t) (PropertyList :- a :- t)+ -> (forall t. Grammar Position (Sexp :- t) (a :- t))+ -> Grammar Position (PropertyList :- t) (PropertyList :- a :- t) (.:) = key -- | Optional property by a key grammar. Infix version of 'optKey'. (.:?) :: Text- -> (forall t. Grammar p (Sexp :- t) (a :- t))- -> Grammar p (PropertyList :- t) (PropertyList :- Maybe a :- t)+ -> (forall t. Grammar Position (Sexp :- t) (a :- t))+ -> Grammar Position (PropertyList :- t) (PropertyList :- Maybe a :- t) (.:?) = optKey -- | Remaining properties grammar. Extracts all key-value pairs and -- applies a grammar on every element. restKeys- :: (forall t. Grammar p (Sexp :- Text :- t) (a :- t))- -> Grammar p (PropertyList :- t) (PropertyList :- [a] :- t)+ :: (forall t. Grammar Position (Sexp :- Text :- t) (a :- t))+ -> Grammar Position (PropertyList :- t) (PropertyList :- [a] :- t) restKeys f = iso coerce coerce >>> onHead (Traverse (sealed (Flip pair >>> f) >>> Step)) >>>
test/Main.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -11,6 +12,8 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeSynonymInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-}+ module Main (main) where import Prelude hiding ((.), id)@@ -20,17 +23,18 @@ #endif import Control.Category+import Data.ByteString.Lazy.UTF8 (fromString)+import Data.Char import Data.Scientific import Data.Semigroup-import Data.ByteString.Lazy.UTF8 (fromString)-import qualified Data.Text as TS 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 Data.Text.Prettyprint.Doc (Pretty, pretty) import Language.Sexp.Located as Sexp import Language.Sexp () -- for Show instance@@ -41,9 +45,56 @@ 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)+ ])+ , pure (AtomSymbol ":foo")+ , 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) @@ -168,6 +219,10 @@ 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 ] @@ -227,6 +282,21 @@ , 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"])@@ -241,7 +311,7 @@ `sexpEq` Right (Modified Quote (Symbol "foo")) , testCase "hashed" $ parseSexp' "#foo"- `sexpEq` Right (Symbol "#foo")+ `sexpEq` Right (Modified Hash (Symbol "foo")) , testCase "keyword" $ parseSexp' ":foo" `sexpEq` Right (Symbol ":foo")@@ -252,6 +322,7 @@ grammarTests = testGroup "Grammar tests" [ baseTypeTests , listTests+ , prefixTests , dictTests , revStackPrismTests , parseTests@@ -385,7 +456,7 @@ , testCase "hashed" $ fromSexp' (hashed (bracketList (rest int)))- (Modified Quote (BracketList [Number 1, Number 2])) `otherEq`+ (Modified Hash (BracketList [Number 1, Number 2])) `otherEq` Right [1, 2] , testCase "backticked" $