packages feed

comparse (empty) → 0.1.0.0

raw patch · 16 files changed

+996/−0 lines, 16 filesdep +basedep +comparsedep +mtlsetup-changed

Dependencies added: base, comparse, mtl, tasty, tasty-hunit, transformers

Files

+ CHANGELOG.md view
@@ -0,0 +1,8 @@+# Changelog for comparse++## Unreleased changes++- `ParserT` monad transformer+- Basic combinators+- `Data.Stream.StringLines` to parse multi-line `String` input+- `Parser` and `StringParser` type aliases
+ Control/Monad/Parser.hs view
@@ -0,0 +1,28 @@+module Control.Monad.Parser
+  ( module Control.Monad.Parser.Class,
+    ParserT (..),
+    ParseResult (..),
+    ParseError (..),
+    ErrorDesc (..),
+    Parser,
+    StringParser,
+    runParser,
+    runStringParser,
+  )
+where
+
+import Control.Monad.Identity
+import Control.Monad.Parser.Class
+import Control.Monad.Trans.Parser
+import Data.Stream.StringLines (StringLines)
+import qualified Data.Stream.StringLines as StringLines
+
+type Parser s a = ParserT s Identity a
+
+runParser :: Parser s a -> s -> ParseResult a s
+runParser p = runIdentity . runParserT p
+
+type StringParser a = Parser StringLines a
+
+runStringParser :: StringParser a -> String -> ParseResult a StringLines
+runStringParser p s = runIdentity $ runParserT p $ StringLines.fromString s
+ Control/Monad/Parser/Class.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FunctionalDependencies #-}
+
+module Control.Monad.Parser.Class
+  ( module Control.Monad.Parser.Class,
+  )
+where
+
+import Control.Applicative ((<**>))
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Stream
+
+infixl 3 <|>
+
+infixl 1 <?>
+
+-- | A monad with parsing capabilities.
+class Monad m => MonadParser s m | m -> s where
+  -- | The current input stream.
+  parseStream :: m s
+
+  -- | Replace the input stream.
+  setParseStream :: s -> m ()
+
+  -- | A parser that always fails.
+  noParse :: m a
+
+  -- | A parser that returns the next item.
+  item :: m (Item s)
+
+  -- | @followedBy p@ is a parser that succeeds if @p@ succeeds, but it does not
+  -- consume any input.
+  followedBy :: m a -> m ()
+
+  -- | @notFollowedBy p@ is a parser that only succeeds if @p@ fails. This
+  -- parser will not consume any input.
+  notFollowedBy :: m a -> m ()
+
+  -- | @try p@ is a parser that does everything like @p@, except it forcefully
+  -- resets the position of any error reported by @p@ to the current position.
+  try :: m a -> m a
+
+  -- | @p <|> q@ is a parser that is equivalent to @p@ when @p@ succeeds and
+  -- @q@ when @p@ fails to parse anything.
+  (<|>) :: m a -> m a -> m a
+
+  -- | @p <?> msg@ is a parser that behaves like @p@, but when @p@ fails, it
+  -- reports an error indicating that @msg@ was the expected input.
+  (<?>) :: m a -> String -> m a
+
+-- | Parser that succeeds if the stream is empty. Does not consume any items.
+eof :: MonadParser s m => m ()
+eof = notFollowedBy item <?> "end of input"
+
+-- | Fail with an "expected" message.
+expected :: MonadParser s m => String -> m a
+expected s = noParse <?> s
+
+-- | Succeeds only if the value parsed by the parser satisfies the predicate.
+satisfy :: MonadParser s m => m a -> (a -> Bool) -> m a
+satisfy p f = try $ do
+  i <- p
+  if f i
+    then return i
+    else noParse
+
+-- | Parse a single item satisfying the given predicate.
+match :: MonadParser s m => (Item s -> Bool) -> m (Item s)
+match = satisfy item
+
+-- | Make a parser optional.
+optional :: MonadParser s m => m a -> m (Maybe a)
+optional p = Just <$> p <|> pure Nothing
+
+-- | Try a series of parsers in order, returning the first one that succeeds.
+choice :: MonadParser s m => [m a] -> m a
+choice = foldr (<|>) noParse
+
+-- | Try to run the given parser as many times as possible.
+many :: MonadParser s m => m a -> m [a]
+many p = ((:) <$> p <*> many p) <|> pure []
+
+-- | Try to run the given parser as many times as possible, but at least once.
+-- The result is returned as a regular list, but is guaranteed to be non-empty.
+many1 :: MonadParser s m => m a -> m [a]
+many1 p = (:) <$> p <*> many p
+
+-- | Try to run the given parser as many times as possible, but at least once.
+some :: MonadParser s m => m a -> m (NonEmpty a)
+some p = (:|) <$> p <*> many p
+
+-- | Parse a non-empty series of @a@ separated by @b@s (without a trailing @b@).
+sepBy1 :: MonadParser s m => m a -> m b -> m (NonEmpty a)
+sepBy1 a b = (:|) <$> a <*> many (b *> a)
+
+-- | Parse a potentially empty series of @a@ separated by @b@s (without a
+-- trailing @b@).
+sepBy :: MonadParser s m => m a -> m b -> m [a]
+sepBy a b = NonEmpty.toList <$> sepBy1 a b <|> pure []
+
+-- | Parse any value equal to @a@.
+like :: (MonadParser s m, Eq (Item s), Show (Item s)) => Item s -> m (Item s)
+like a = item `satisfy` (== a) <?> show a
+
+-- | Parse any value not equal to @a@.
+unlike :: (MonadParser s m, Eq (Item s), Show (Item s)) => Item s -> m (Item s)
+unlike a = item `satisfy` (/= a) <?> "anything but " ++ show a
+
+-- | Parse a continuous sequence of items equal to the given one.
+string ::
+  (MonadParser s m, Eq (Item s), Show (Item s)) =>
+  [Item s] ->
+  m [Item s]
+string [] = return []
+string (x : xs) = like x >> string xs >> return (x : xs)
+
+-- | Parse any value equal to at least one element of the given list.
+oneOf :: (MonadParser s m, Eq (Item s), Show (Item s)) => [Item s] -> m (Item s)
+oneOf l = item `satisfy` (`elem` l) <?> "one of " ++ show l
+
+-- | Parse any value not equivalent to any element of the given list.
+-- For a version that accepts non-Show items, see @noneOf'@.
+noneOf ::
+  (MonadParser s m, Eq (Item s), Show (Item s)) =>
+  [Item s] ->
+  m (Item s)
+noneOf l = item `satisfy` (`notElem` l) <?> "none of " ++ show l
+
+-- | @chainl1 p op@ Parse a chain of *one* or more occurrences of @p@,
+-- separated by @op@. Return a value obtained by a left associative application
+-- of all functions returned by @op@ to the values returned by @p@.
+--
+-- This is particularly useful for parsing left associative infix operators.
+chainl1 :: MonadParser s m => m a -> m (a -> a -> a) -> m a
+chainl1 p op = scan
+  where
+    scan = p <**> rst
+    rst = (\f y g x -> g (f x y)) <$> op <*> p <*> rst <|> pure id
+
+-- | @chainr1 p op@ Parse a chain of *one* or more occurrences of @p@,
+-- separated by @op@. Return a value obtained by a right associative application
+-- of all functions returned by @op@ to the values returned by @p@.
+--
+-- This is particularly useful for parsing right associative infix operators.
+chainr1 :: MonadParser s m => m a -> m (a -> a -> a) -> m a
+chainr1 p op = scan
+  where
+    scan = p <**> rst
+    rst = (flip <$> op <*> scan) <|> pure id
+
+-- | Run a parser on a different stream of items.
+withInput :: MonadParser s m => s -> m a -> m (a, s)
+withInput s' p = do
+  s <- parseStream
+  setParseStream s'
+  x <- p
+  s'' <- parseStream
+  setParseStream s
+  return (x, s'')
+ Control/Monad/Trans/Parser.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Control.Monad.Trans.Parser
+  ( ParserT (..),
+    ParseResult (..),
+    ParseError (..),
+    ErrorDesc (..),
+  )
+where
+
+import Control.Monad
+import Control.Monad.Cont.Class
+import Control.Monad.Except
+import Control.Monad.Parser.Class
+import Control.Monad.Reader.Class
+import Control.Monad.State.Class
+import Data.List (union)
+import Data.Stream (Stream (..))
+
+data ErrorDesc = Expected String | Note String deriving (Eq)
+
+data ParseError p = ParseError p [ErrorDesc]
+
+makeOrList :: [String] -> String
+makeOrList [] = ""
+makeOrList [x] = x
+makeOrList [a, b] = a ++ ", or " ++ b
+makeOrList (x : xs) = x ++ ", " ++ makeOrList xs
+
+instance Show p => Show (ParseError p) where
+  show (ParseError p d) =
+    show p ++ "\n" ++ showExpects expects ++ showNotes notes
+    where
+      expects = [e | Expected e <- d]
+      notes = [n | Note n <- d]
+      showExpects [] = ""
+      showExpects es = "expected " ++ makeOrList es ++ "\n"
+      showNotes [] = ""
+      showNotes (n : ns) = "note: " ++ n ++ "\n" ++ showNotes ns
+
+joinErrors :: Ord p => ParseError p -> ParseError p -> ParseError p
+joinErrors e1@(ParseError p1 d1) e2@(ParseError p2 d2)
+  | null d1 && not (null d2) = e2
+  | null d2 && not (null d1) = e1
+  | p1 > p2 = e1
+  | p1 < p2 = e2
+  | otherwise = ParseError p1 (d1 `union` d2)
+
+emptyError :: Stream s => s -> ParseError (Pos s)
+emptyError s = ParseError (getPos s) []
+
+data ParseResult v s
+  = Parsed v s (ParseError (Pos s))
+  | NoParse (ParseError (Pos s))
+
+instance (Show v, Show (Pos s)) => Show (ParseResult v s) where
+  show (Parsed v _ _) = show v
+  show (NoParse e) = show e
+
+-- | Parser monad transformer.
+newtype ParserT s m a = ParserT {runParserT :: s -> m (ParseResult a s)}
+
+instance Functor m => Functor (ParserT s m) where
+  fmap f p = ParserT (fmap t . runParserT p)
+    where
+      t (NoParse e) = NoParse e
+      t (Parsed a s e) = Parsed (f a) s e
+
+instance (Stream s, Applicative m, Monad m) => Applicative (ParserT s m) where
+  pure a = ParserT $ \s -> pure (Parsed a s $ emptyError s)
+  mf <*> mx = ParserT $ runParserT mf >=> pmf
+    where
+      pmf (NoParse e) = pure $ NoParse e
+      pmf (Parsed f s e1) = pmx f e1 <$> runParserT mx s
+      pmx _ e1 (NoParse e2) = NoParse $ joinErrors e1 e2
+      pmx f e1 (Parsed x s' e2) = Parsed (f x) s' $ joinErrors e1 e2
+
+instance (Stream s, Monad m) => Monad (ParserT s m) where
+  return = pure
+  m >>= f = ParserT $ runParserT m >=> first
+    where
+      first (NoParse e) = pure $ NoParse e
+      first (Parsed r s e) = second e <$> runParserT (f r) s
+      second e1 (NoParse e2) = NoParse $ joinErrors e1 e2
+      second e1 (Parsed r s e2) = Parsed r s $ joinErrors e1 e2
+
+instance (Applicative m, Monad m, Stream s) => MonadFail (ParserT s m) where
+  fail msg = ParserT $ \s -> pure $ NoParse $ ParseError (getPos s) [Note msg]
+
+instance (Monad m, Stream s) => MonadParser s (ParserT s m) where
+  parseStream = ParserT $ \s -> pure $ Parsed s s $ emptyError s
+
+  setParseStream s = ParserT $ \_ -> pure $ Parsed () s $ emptyError s
+
+  noParse = ParserT $ \s -> pure $ NoParse $ emptyError s
+
+  item = ParserT $ pure . eat
+    where
+      eat s = case next s of
+        Nothing -> NoParse $ emptyError s
+        Just (x, s') -> Parsed x s' $ emptyError s'
+
+  notFollowedBy p = ParserT $ \s -> go s <$> runParserT p s
+    where
+      go s (NoParse _) = Parsed () s $ emptyError s
+      go s _ = NoParse $ emptyError s
+
+  followedBy p = ParserT $ \s -> do
+    r <- runParserT p s
+    pure $ case r of
+      (NoParse e) -> NoParse e
+      _ -> Parsed () s $ emptyError s
+
+  try p = ParserT $ \s -> do
+    r <- runParserT p s
+    pure $ case r of
+      NoParse _ -> NoParse $ emptyError s
+      _ -> r
+
+  p <|> q = ParserT $ \s -> runParserT p s >>= first s
+    where
+      first _ (Parsed a s' e) = pure $ Parsed a s' e
+      first s (NoParse e) = runParserT q s >>= second e
+      second e1 (Parsed a s' e2) = pure $ Parsed a s' $ joinErrors e1 e2
+      second e1 (NoParse e2) = pure $ NoParse $ joinErrors e1 e2
+
+  p <?> n = ParserT $ \s -> labelize (getPos s) <$> runParserT p s
+    where
+      labelize here (Parsed a s e) = Parsed a s $ name here e
+      labelize here (NoParse e) = NoParse $ name here e
+      name here e@(ParseError pos _)
+        | pos > here = e
+        | otherwise = ParseError here [Expected n]
+
+instance Stream s => MonadTrans (ParserT s) where
+  lift m = ParserT $ \s -> do
+    a <- m
+    pure $ Parsed a s $ emptyError s
+
+instance (Stream s, MonadIO m) => MonadIO (ParserT s m) where
+  liftIO = lift . liftIO
+
+instance (Stream s, MonadState s' m) => MonadState s' (ParserT s m) where
+  get = lift get
+  put s = lift $ put s
+
+instance (Stream s, MonadError e m) => MonadError e (ParserT s m) where
+  throwError e = ParserT $ const $ throwError e
+  catchError m f = ParserT $ \s -> do
+    runParserT m s `catchError` \e -> runParserT (f e) s
+
+instance (Stream s, MonadReader r m) => MonadReader r (ParserT s m) where
+  ask = lift ask
+  local f st = ParserT $ \s -> local f (runParserT st s)
+
+instance (Stream s, MonadCont m) => MonadCont (ParserT s m) where
+  callCC f =
+    ParserT $
+      \s -> callCC $
+        \k ->
+          runParserT
+            ( f $
+                \a -> ParserT $ \s' -> k $ Parsed a s' $ emptyError s'
+            )
+            s
+ Data/Stream.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Data.Stream
+  ( Stream (..),
+  )
+where
+
+import Data.Data (Proxy (..))
+import Data.Kind (Type)
+
+class Ord (Pos s) => Stream s where
+  type Item s :: Type
+  type Chunk s :: Type
+  type Pos s :: Type
+
+  next :: s -> Maybe (Item s, s)
+  nextWhile :: (Item s -> Bool) -> s -> (Chunk s, s)
+  nextN :: Int -> s -> (Chunk s, s)
+
+  makeChunk :: Proxy s -> [Item s] -> Chunk s
+  unmakeChunk :: Proxy s -> Chunk s -> [Item s]
+
+  getPos :: s -> Pos s
+ Data/Stream/StringLines.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE TypeFamilies #-}
+
+module Data.Stream.StringLines
+  ( StringPos (..),
+    StringLines (..),
+    fromString,
+  )
+where
+
+import Data.Stream (Stream (..))
+
+data StringPos = StringPos Int Int String
+
+instance Show StringPos where
+  show (StringPos l c ls) =
+    lnum ++ ":" ++ cnum ++ ":\n" ++ snippet
+    where
+      lnum = show (l + 1)
+      cnum = show (c + 1)
+      gut = replicate (length lnum) ' ' ++ " | "
+      ngut = lnum ++ " | "
+      snippet = gut ++ "\n" ++ ngut ++ ls ++ "\n" ++ gut ++ cursor
+      cursor = replicate c ' ' ++ "^"
+
+instance Eq StringPos where
+  (StringPos l1 c1 _) == (StringPos l2 c2 _) = l1 == l2 && c1 == c2
+
+instance Ord StringPos where
+  compare (StringPos l1 c1 _) (StringPos l2 c2 _) =
+    case compare l1 l2 of
+      EQ -> compare c1 c2
+      x -> x
+
+start :: String -> StringPos
+start s = StringPos 0 0 $ takeWhile (/= '\n') s
+
+adv :: StringPos -> StringPos
+adv (StringPos l c s) = StringPos l (c + 1) s
+
+nextl :: StringPos -> String -> StringPos
+nextl (StringPos l _ _) s = StringPos (l + 1) 0 (takeWhile (/= '\n') s)
+
+data StringLines = StringLines String StringPos deriving (Eq, Show)
+
+instance Stream StringLines where
+  type Item StringLines = Char
+  type Chunk StringLines = String
+  type Pos StringLines = StringPos
+
+  next (StringLines [] _) = Nothing
+  next (StringLines ('\n' : xs) p) = Just ('\n', StringLines xs (nextl p xs))
+  next (StringLines (x : xs) p) = Just (x, StringLines xs (adv p))
+
+  nextWhile f s = case next s of
+    Nothing -> ([], s)
+    Just (x, s') | f x -> (x : xs, s'') where (xs, s'') = nextWhile f s'
+    _ -> ([], s)
+
+  nextN 0 s = ([], s)
+  nextN n s = case next s of
+    Nothing -> ([], s)
+    Just (x, s') -> (x : xs, s'') where (xs, s'') = nextN (n - 1) s'
+
+  makeChunk _ = id
+  unmakeChunk _ = id
+
+  getPos (StringLines _ p) = p
+
+fromString :: String -> StringLines
+fromString s = StringLines s (start s)
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright nasso (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of nasso nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,86 @@+# comparse++[![Tests](https://github.com/nasso/comparse/actions/workflows/tests.yml/badge.svg)](https://github.com/nasso/comparse/actions/workflows/tests.yml)++`comparse` is a parser combinator library supporting arbitrary input types.+Combinators do not care about the exact type of the input stream, allowing you+to use them on your own data source, as long as it supports a specific set of+operations. The `ParserT` monad transformer can wrap around another monad,+allowing you to easily implement features such as tracing, context-sensitive+parsing, state machines, and so on. `comparse` does its best to provide+meaningful error messages without sacrificing performances or the developer+experience when writing parsers.++## Example++The following example shows how to use the `comparse` library to parse a subset+of the [JSON](https://en.wikipedia.org/wiki/JSON) data format:++```hs+import Control.Monad (void)+import Control.Monad.Parser+import Data.Char (isAlpha, isDigit)+import Data.Stream.StringLines++data JValue+  = JString String+  | JNumber Int+  | JBool Bool+  | JNull+  | JObject [(String, JValue)]+  | JArray [JValue]+  deriving (Show)++main :: IO ()+main = interact $ show . parseJValue++parseJValue :: String -> Maybe JValue+parseJValue s =+  case runStringParser (json <* eof) s of+    Parsed v (StringLines "" _) _ -> Just v+    _ -> Nothing++lexeme :: StringParser a -> StringParser a+lexeme p = spaces *> p <* spaces+  where+    spaces = void $ many $ oneOf " \n\r\t"++symbol :: String -> StringParser String+symbol = lexeme . string++json :: StringParser JValue+json =+  JString <$> stringLiteral+    <|> JNumber <$> number+    <|> JBool <$> bool+    <|> JNull <$ symbol "null"+    <|> JObject <$> object+    <|> JArray <$> array++stringLiteral :: StringParser String+stringLiteral = lexeme $ like '"' *> many (unlike '\"') <* like '"'++number :: StringParser Int+number =+  lexeme+    ( read <$> ((:) <$> like '-' <*> many1 digit)+        <|> read <$> (optional (like '+') *> many1 digit)+    )+    <* notFollowedBy (match isAlpha)+  where+    digit = match isDigit++bool :: StringParser Bool+bool = True <$ symbol "true" <|> False <$ symbol "false"++object :: StringParser [(String, JValue)]+object =+  symbol "{"+    *> sepBy ((,) <$> (stringLiteral <* symbol ":") <*> json) (symbol ",")+    <* symbol "}"++array :: StringParser [JValue]+array = symbol "[" *> sepBy json (symbol ",") <* symbol "]"+```++For more examples, please refer to the [tests](test/Parsing.hs).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ comparse.cabal view
@@ -0,0 +1,67 @@+cabal-version: 1.12
++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name:           comparse+version:        0.1.0.0+synopsis:       An unopiniated parser combinators library.+description:    Please see the README on GitHub at <https://github.com/nasso/comparse#readme>+category:       Parsing+homepage:       https://github.com/nasso/comparse#readme+bug-reports:    https://github.com/nasso/comparse/issues+author:         nasso+maintainer:     nasso <nassomails@gmail.com>+copyright:      2021 nasso+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    CHANGELOG.md+    LICENSE+    README.md++source-repository head+  type: git+  location: https://github.com/nasso/comparse++library+  exposed-modules:+      Control.Monad.Parser+      Control.Monad.Parser.Class+      Control.Monad.Trans.Parser+      Data.Stream+      Data.Stream.StringLines+  other-modules:+      Paths_comparse+  hs-source-dirs:+      ./+  ghc-options: -Wall+  build-depends:+      base ==4.14.*+    , mtl ==2.2.*+    , transformers ==0.5.*+  default-language: Haskell2010++test-suite comparse-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Json+      Parsing+      Stream+      Stream.Generic+      Stream.StringLines+      Paths_comparse+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall+  build-depends:+      base ==4.14.*+    , comparse+    , mtl ==2.2.*+    , tasty ==1.4.*+    , tasty-hunit ==0.10.*+    , transformers ==0.5.*+  default-language: Haskell2010
+ test/Json.hs view
@@ -0,0 +1,107 @@+module Json (jsonTests) where++import Control.Monad (void)+import Control.Monad.Parser+import Data.Char (isAlpha, isDigit)+import Data.Stream.StringLines+import Test.Tasty+import Test.Tasty.HUnit++data JValue+  = JString String+  | JNumber Int+  | JBool Bool+  | JNull+  | JObject [(String, JValue)]+  | JArray [JValue]+  deriving (Eq, Show)++jsonTests :: [TestTree]+jsonTests =+  [ testCase "string" $ parseJValue "\"foo\"" @?= Just (JString "foo"),+    testCase "empty string" $ parseJValue "\"\"" @?= Just (JString ""),+    testCase "number" $ parseJValue "123" @?= Just (JNumber 123),+    testCase "negative number" $ parseJValue "-123" @?= Just (JNumber (-123)),+    testCase "positive number" $ parseJValue "+123" @?= Just (JNumber 123),+    testCase "bool true" $ parseJValue "true" @?= Just (JBool True),+    testCase "bool false" $ parseJValue "false" @?= Just (JBool False),+    testCase "null" $ parseJValue "null" @?= Just JNull,+    testCase "object" $+      parseJValue "{\"foo\": \"bar\"}"+        @?= Just (JObject [("foo", JString "bar")]),+    testCase "empty object" $ parseJValue "{}" @?= Just (JObject []),+    testCase "array" $+      parseJValue "[1, 2, 3]"+        @?= Just (JArray [JNumber 1, JNumber 2, JNumber 3]),+    testCase "empty array" $ parseJValue "[]" @?= Just (JArray []),+    testCase "nested array" $+      parseJValue "[1, [2, 3], 4]"+        @?= Just (JArray [JNumber 1, JArray [JNumber 2, JNumber 3], JNumber 4]),+    testCase "nested object" $+      parseJValue "{\"foo\": {\"bar\": \"baz\"}}"+        @?= Just (JObject [("foo", JObject [("bar", JString "baz")])]),+    testCase "nested object with array" $+      parseJValue "{\"foo\": {\"bar\": [1, 2, 3]}}"+        @?= Just+          ( JObject+              [ ( "foo",+                  JObject+                    [ ("bar", JArray [JNumber 1, JNumber 2, JNumber 3])+                    ]+                )+              ]+          ),+    testCase "invalid input" $ parseJValue "foo" @?= Nothing,+    testCase "weird spacing" $+      parseJValue "   {\t  \n\"foo\"\r \t: \"bar\"\n }  \t"+        @?= Just (JObject [("foo", JString "bar")]),+    testCase "number followed by letter" $ parseJValue "123a" @?= Nothing+  ]++parseJValue :: String -> Maybe JValue+parseJValue s =+  case runStringParser (json <* eof) s of+    Parsed v (StringLines "" _) _ -> Just v+    _ -> Nothing++lexeme :: StringParser a -> StringParser a+lexeme p = spaces *> p <* spaces+  where+    spaces = void $ many $ oneOf " \n\r\t"++symbol :: String -> StringParser String+symbol = lexeme . string++json :: StringParser JValue+json =+  JString <$> stringLiteral+    <|> JNumber <$> number+    <|> JBool <$> bool+    <|> JNull <$ symbol "null"+    <|> JObject <$> object+    <|> JArray <$> array++stringLiteral :: StringParser String+stringLiteral = lexeme $ like '"' *> many (unlike '\"') <* like '"'++number :: StringParser Int+number =+  lexeme+    ( read <$> ((:) <$> like '-' <*> many1 digit)+        <|> read <$> (optional (like '+') *> many1 digit)+    )+    <* notFollowedBy (match isAlpha)+  where+    digit = match isDigit++bool :: StringParser Bool+bool = True <$ symbol "true" <|> False <$ symbol "false"++object :: StringParser [(String, JValue)]+object =+  symbol "{"+    *> sepBy ((,) <$> (stringLiteral <* symbol ":") <*> json) (symbol ",")+    <* symbol "}"++array :: StringParser [JValue]+array = symbol "[" *> sepBy json (symbol ",") <* symbol "]"
+ test/Parsing.hs view
@@ -0,0 +1,152 @@+module Parsing where
+
+import Control.Monad.Parser
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Stream.StringLines
+import Test.Tasty
+import Test.Tasty.HUnit
+
+parse :: StringParser a -> String -> Maybe (a, String)
+parse p s =
+    case runStringParser p s of
+        Parsed v (StringLines rest _) _ -> Just (v, rest)
+        _ -> Nothing
+
+parserTests :: [TestTree]
+parserTests =
+    [ coreParserTests
+    , coreCombinatorsTests
+    , derivedParserTests
+    , derivedCombinatorsTests
+    ]
+
+coreParserTests :: TestTree
+coreParserTests =
+    testGroup
+        "Core parsers"
+        [ testCase "item returns next char" $
+            parse item "hi" @?= Just ('h', "i")
+        , testCase "item fails on eof" $
+            parse item "" @?= Nothing
+        , testCase "eof succeeds on eof" $
+            parse eof "" @?= Just ((), "")
+        , testCase "eof fails on non-eof" $
+            parse eof "hi" @?= Nothing
+        , testCase "noParse fails with non-empty input" $
+            parse (noParse :: StringParser ()) "hi" @?= Nothing
+        , testCase "noParse fails with empty input" $
+            parse (noParse :: StringParser ()) "" @?= Nothing
+        ]
+
+coreCombinatorsTests :: TestTree
+coreCombinatorsTests =
+    testGroup
+        "Core combinators"
+        [ testCase "followedBy fails when parser fails" $
+            parse (followedBy $ like 'h') "ello" @?= Nothing
+        , testCase "followedBy succeeds without consuming input" $
+            parse (followedBy $ like 'h') "hi" @?= Just ((), "hi")
+        , testCase "notFollowedBy fails when parser succeeds" $
+            parse (notFollowedBy $ like 'h') "hi" @?= Nothing
+        , testCase "notFollowedBy succeeds without consuming input" $
+            parse (notFollowedBy $ like 'g') "hi" @?= Just ((), "hi")
+        , testCase "`a <|> b` returns a when it succeeds" $
+            parse (like 'h' <|> like 'g') "hi" @?= Just ('h', "i")
+        , testCase "`a <|> b` returns b when a fails" $
+            parse (like 'g' <|> like 'h') "hi" @?= Just ('h', "i")
+        , testCase "`a <|> b` fails when both fail" $
+            parse (like 'g' <|> like 'i') "hi" @?= Nothing
+        ]
+
+derivedParserTests :: TestTree
+derivedParserTests =
+    testGroup
+        "Derived parsers"
+        [ testCase "like succeeds on matching item" $
+            parse (like 'h') "hi" @?= Just ('h', "i")
+        , testCase "like fails on non-matching item" $
+            parse (like 'h') "pi" @?= Nothing
+        , testCase "like fails on eof" $
+            parse (like 'h') "" @?= Nothing
+        , testCase "unlike succeeds on non-matching item" $
+            parse (unlike 'g') "hi" @?= Just ('h', "i")
+        , testCase "unlike fails on matching item" $
+            parse (unlike 'h') "hi" @?= Nothing
+        , testCase "string succeeds on matching string" $
+            parse (string "hell") "hello" @?= Just ("hell", "o")
+        , testCase "string fails on non-matching string" $
+            parse (string "hello") "hi" @?= Nothing
+        , testCase "`string \"\"` succeeds on arbitrary input" $
+            parse (string "") "hi" @?= Just ("", "hi")
+        , testCase "`string \"\"` succeeds on empty input" $
+            parse (string "") "" @?= Just ("", "")
+        ]
+
+derivedCombinatorsTests :: TestTree
+derivedCombinatorsTests =
+    testGroup
+        "Derived combinators"
+        [ testCase "satisfy succeeds on matching char" $
+            parse (satisfy item (== 'h')) "hi" @?= Just ('h', "i")
+        , testCase "satisfy fails on non-matching char" $
+            parse (satisfy item (== 'g')) "hi" @?= Nothing
+        , testCase "optional returns Nothing on eof" $
+            parse (optional item) "" @?= Just (Nothing, "")
+        , testCase "optional returns Nothing on failure" $
+            parse (optional $ like 'g') "hi" @?= Just (Nothing, "hi")
+        , testCase "optional returns Just on success" $
+            parse (optional item) "hi" @?= Just (Just 'h', "i")
+        , testCase "many returns empty list on eof" $
+            parse (many item) "" @?= Just ([], "")
+        , testCase "many returns list of items" $
+            parse (many item) "hi" @?= Just ("hi", "")
+        , testCase "many1 fails on eof" $
+            parse (many1 item) "" @?= Nothing
+        , testCase "many1 returns non-empty list of items" $
+            parse (many1 item) "hi" @?= Just ("hi", "")
+        , testCase "some fails on eof" $
+            parse (some item) "" @?= Nothing
+        , testCase "some returns NonEmpty list of items" $
+            parse (some item) "hi" @?= Just (NonEmpty.fromList "hi", "")
+        , testCase "choice takes first successful parser" $
+            parse (choice [like 'h', like 'g']) "hi" @?= Just ('h', "i")
+        , testCase "choice with empty parser list fails" $
+            parse (choice [] :: StringParser ()) "hi" @?= Nothing
+        , testCase "choice with no matching parser fails" $
+            parse (choice [like 'g', like 'j']) "hi" @?= Nothing
+        , testCase "sepBy matches empty list" $
+            parse (sepBy item $ like ',') "" @?= Just ([], "")
+        , testCase "sepBy matches list of one item" $
+            parse (sepBy item $ like ',') "hi" @?= Just ("h", "i")
+        , testCase "sepBy matches list of many items" $
+            parse (sepBy item $ like ',') "h,e,llo" @?= Just ("hel", "lo")
+        , testCase "sepBy doesn't consume trailing separator" $
+            parse (sepBy item $ like ',') "h,i," @?= Just ("hi", ",")
+        , testCase "sepBy1 doesn't match empty list" $
+            parse (sepBy1 item $ like ',') "" @?= Nothing
+        , testCase "sepBy1 matches list of one item" $
+            parse (sepBy1 item $ like ',') "hi"
+                @?= Just (NonEmpty.fromList "h", "i")
+        , testCase "sepBy1 matches list of many items" $
+            parse (sepBy1 item $ like ',') "h,e,llo"
+                @?= Just (NonEmpty.fromList "hel", "lo")
+        , testCase "sepBy1 doesn't consume trailing separator" $
+            parse (sepBy1 item $ like ',') "h,i,"
+                @?= Just (NonEmpty.fromList "hi", ",")
+        , testCase "oneOf fails when item doesn't match any" $
+            parse (oneOf "gj") "hi" @?= Nothing
+        , testCase "oneOf succeeds when item matches one" $
+            parse (oneOf "gh") "hi" @?= Just ('h', "i")
+        , testCase "oneOf succeeds when item matches many" $
+            parse (oneOf "ghh") "hi" @?= Just ('h', "i")
+        , testCase "oneOf fails with empty class" $
+            parse (oneOf "") "hi" @?= Nothing
+        , testCase "noneOf succeeds when item doesn't match any" $
+            parse (noneOf "gj") "hi" @?= Just ('h', "i")
+        , testCase "noneOf fails when item matches one" $
+            parse (noneOf "gh") "hi" @?= Nothing
+        , testCase "noneOf fails when item matches many" $
+            parse (noneOf "ghh") "hi" @?= Nothing
+        , testCase "noneOf succeeds with empty class" $
+            parse (noneOf "") "hi" @?= Just ('h', "i")
+        ]
+ test/Spec.hs view
@@ -0,0 +1,14 @@+import Json
+import Parsing
+import Stream
+import Test.Tasty
+
+main :: IO ()
+main =
+  defaultMain $
+    testGroup
+      "Tests"
+      [ testGroup "Streams" streamTests,
+        testGroup "Parsers" parserTests,
+        testGroup "Json" jsonTests
+      ]
+ test/Stream.hs view
@@ -0,0 +1,7 @@+module Stream (streamTests) where
+
+import Stream.StringLines (stringStreamTests)
+import Test.Tasty
+
+streamTests :: [TestTree]
+streamTests = [stringStreamTests]
+ test/Stream/Generic.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Stream.Generic (genericStreamTests) where
+
+import Data.Stream
+import Data.String (IsString, fromString)
+import Test.Tasty
+import Test.Tasty.HUnit
+
+genericStreamTests ::
+  ( Stream s
+  , Show s
+  , Eq s
+  , Item s ~ Char
+  , IsString (Chunk s)
+  , Eq (Chunk s)
+  , Show (Chunk s)
+  ) =>
+  (Int -> Int -> String -> s) ->
+  [TestTree]
+genericStreamTests makeAt =
+  [ testCase "next on same line gives next character" $
+      next (make "foo\nbar\nbaz") @?= Just ('f', makeAt 0 1 "oo\nbar\nbaz")
+  , testCase "next at end of line gives linefeed" $
+      next (make "\nbar\nbaz") @?= Just ('\n', makeAt 1 0 "bar\nbaz")
+  , testCase "next before end of line stops before linefeed" $
+      next (make "o\nbar\nbaz") @?= Just ('o', makeAt 0 1 "\nbar\nbaz")
+  , testCase "next at end of file gives Nothing" $
+      next (make "") @?= Nothing
+  , testCase "nextWhile gives empty chunk when nothing matches" $
+      nextWhile (== 'x') (make "foo") @?= (fromString "", makeAt 0 0 "foo")
+  , testCase "nextWhile gives matching chunk" $
+      nextWhile (== 'o') (make "oof") @?= (fromString "oo", makeAt 0 2 "f")
+  , testCase "nextWhile can consume all stream" $
+      nextWhile (const True) (make "foo") @?= (fromString "foo", makeAt 0 3 "")
+  , testCase "nextWhile can consume multiple lines" $
+      nextWhile (const True) (make "foo\nbar\nbaz")
+        @?= (fromString "foo\nbar\nbaz", makeAt 2 3 "")
+  , testCase "nextN consume nothing when N=0" $
+      nextN 0 (make "foo") @?= (fromString "", makeAt 0 0 "foo")
+  , testCase "nextN consume one character when N=1" $
+      nextN 1 (make "foo") @?= (fromString "f", makeAt 0 1 "oo")
+  , testCase "nextN consume multiple characters when N>1" $
+      nextN 3 (make "foo") @?= (fromString "foo", makeAt 0 3 "")
+  , testCase "nextN stops at end of file" $
+      nextN 3 (make "") @?= (fromString "", makeAt 0 0 "")
+  , testCase "nextN counts linefeeds" $
+      nextN 2 (make "\nfoo\nbar") @?= (fromString "\nf", makeAt 1 1 "oo\nbar")
+  ]
+ where
+  make = makeAt 0 0
+ test/Stream/StringLines.hs view
@@ -0,0 +1,21 @@+module Stream.StringLines (stringStreamTests) where
+
+import Data.Stream
+import Data.Stream.StringLines (StringLines (..), StringPos (StringPos))
+import qualified Data.Stream.StringLines as StringLines
+import Stream.Generic (genericStreamTests)
+import Test.Tasty
+import Test.Tasty.HUnit
+
+stringStreamTests :: TestTree
+stringStreamTests =
+  testGroup "String" $
+    [ testCase "fromString" $
+        StringLines.fromString "foo\nbar\nbaz"
+          @?= StringLines "foo\nbar\nbaz" (StringPos 0 0 "foo")
+    , testCase "getPos returns current position" $
+        getPos (makeAt 3 5 "foo\nbar") @?= StringPos 3 5 "foo"
+    ]
+      ++ genericStreamTests makeAt
+ where
+  makeAt l c s = StringLines s $ StringPos l c $ takeWhile (/= '\n') s