packages feed

scanner (empty) → 0.1

raw patch · 14 files changed

+831/−0 lines, 14 filesdep +attoparsecdep +basedep +bytestringsetup-changedbinary-added

Dependencies added: attoparsec, base, bytestring, criterion, hspec, scanner, text

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2016, Yuras Shumovich++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 Yuras Shumovich 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,22 @@+# scanner+Fast non-backtracking incremental combinator parsing for bytestrings++[![Build Status](https://travis-ci.org/Yuras/scanner.svg?branch=master)](https://travis-ci.org/Yuras/scanner)++It is often convinient to use backtracking to parse some sofisticated+input. Unfortunately it kills performance, so usually you should avoid+backtracking.++Often (actually always, but it could be too hard sometimes) you can+implement your parser without any backtracking. It that case all the+bookkeeping usuall parser combinators do becomes unnecessary. The+scanner libarary is designed for such cases. It is often 2 times faster+then attoparsec.++As an example, please checkout redis protocol parser included into the+repo, both using attoparsec and scanner libraries:+https://github.com/Yuras/scanner/tree/master/examples/Redis++Benchmark results:++![Bechmark results](https://raw.githubusercontent.com/Yuras/scanner/master/bench/bench.png)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bench/bench.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE OverloadedStrings #-}++module Main+( main+)+where++import qualified Scanner++import qualified Redis.Reply as Redis+import qualified Redis.Atto+import qualified Redis.Scanner++import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import qualified Data.Attoparsec.ByteString as Atto++import Criterion+import Criterion.Main++main :: IO ()+main = do+  let smallStringInput = "+OK\r\n"+      longStringInput = "+11111111111111111111111111122222222222222222222233333333333333333333333444444444444444444445555555555555555555555666666666666666666677777777777777777777888888888888888888888999999999999999999000000000000000000\r\n"+      intInput = ":123\r\n"+      bulkInput = "$10\r\n0123456789\r\n"+      multiInput = "*3\r\n+A\r\n+B\r\n+C\r\n"+  print (redisByteStringReply smallStringInput)+  print (redisAttoReply smallStringInput)+  print (redisScannerReply smallStringInput)+  print (redisAttoReply intInput)+  print (redisScannerReply intInput)+  print (redisAttoReply bulkInput)+  print (redisScannerReply bulkInput)+  print (redisAttoReply multiInput)+  print (redisScannerReply multiInput)+  defaultMain+    [ bgroup "small string"+      [ bench "Atto" $ whnf redisAttoReply smallStringInput+      , bench "Scanner" $ whnf redisScannerReply smallStringInput+      , bench "ByteString" $ whnf redisByteStringReply smallStringInput+      ]++    , bgroup "long string"+      [ bench "Atto" $ whnf redisAttoReply longStringInput+      , bench "Scanner" $ whnf redisScannerReply longStringInput+      , bench "ByteString" $ whnf redisByteStringReply longStringInput+      ]++    , bgroup "integer"+      [ bench "Atto" $ whnf redisAttoReply intInput+      , bench "Scanner" $ whnf redisScannerReply intInput+      ]++    , bgroup "bulk"+      [ bench "Atto" $ whnf redisAttoReply bulkInput+      , bench "Scanner" $ whnf redisScannerReply bulkInput+      ]++    , bgroup "multi"+      [ bench "Atto" $ whnf redisAttoReply multiInput+      , bench "Scanner" $ whnf redisScannerReply multiInput+      ]+    ]++{-# NOINLINE redisAttoReply #-}+redisAttoReply :: ByteString -> Either String Redis.Reply+redisAttoReply bs = case Atto.parse Redis.Atto.reply bs of+  Atto.Done _ r -> Right r+  Atto.Fail _ _ err -> Left err+  Atto.Partial _ -> Left "Not enough input"++{-# NOINLINE redisScannerReply #-}+redisScannerReply :: ByteString -> Either String Redis.Reply+redisScannerReply bs = case Scanner.scan Redis.Scanner.reply bs of+  Scanner.Done _ r -> Right r+  Scanner.Fail _ err -> Left err+  Scanner.More _ -> Left "Not enought input"++{-# NOINLINE redisByteStringReply #-}+redisByteStringReply :: ByteString -> Either String Redis.Reply+redisByteStringReply bs = case ByteString.uncons bs of+  Just (c, bs') -> case c of+    43 -> let (l, r) = ByteString.span (/= 13) bs'+          in case ByteString.uncons r of+            Just (c', bs'') -> case c' of+              13 -> case ByteString.uncons bs'' of+                Just (c'', _) -> case c'' of+                  10 -> Right (Redis.String l)+                  _ -> Left "Unexpected input"+                Nothing -> Left "Not enough input"+              _ -> Left "Unexpected input"+            Nothing -> Left "Not enought input"+    _ -> Left "Unknown type"+  Nothing -> Left "Not enought input"
+ bench/bench.png view

binary file changed (absent → 22407 bytes)

+ compat/Data/Either.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE CPP #-}++module Data.Either+(+  module Export,+#if MIN_VERSION_base(4,7,0)+#else+  isRight,+  isLeft,+#endif+)+where++import "base" Data.Either as Export++#if MIN_VERSION_base(4,7,0)+#else+isRight :: Either a b -> Bool+isRight (Right _) = True+isRight _ = False++isLeft :: Either a b -> Bool+isLeft = not . isRight+#endif
+ compat/Prelude.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE CPP #-}++module Prelude+(+  module P,++#if MIN_VERSION_base(4,8,0)+#else+  (<$>),+  Monoid(..),+  Applicative(..),+#endif+)+where++#if MIN_VERSION_base(4,6,0)+import "base" Prelude as P+#else+import "base" Prelude as P hiding (catch)+#endif++#if MIN_VERSION_base(4,8,0)+#else+import Data.Functor((<$>))+import Data.Monoid(Monoid(..))+import Control.Applicative(Applicative(..))+#endif
+ examples/Redis/Atto.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}++module Redis.Atto+( reply+)+where++import Redis.Reply++import Prelude hiding (error)+import Data.ByteString (ByteString)+import Data.Attoparsec.ByteString (Parser)+import qualified Data.Attoparsec.ByteString as Atto (takeTill)+import qualified Data.Attoparsec.ByteString.Char8 as Atto hiding (takeTill)+import Control.Monad++{-# INLINE reply #-}+reply :: Parser Reply+reply = do+  c <- Atto.anyChar+  case c of+    '+' -> string+    '-' -> error+    ':' -> integer+    '$' -> bulk+    '*' -> multi+    _ -> fail "Unknown reply type"++{-# INLINE string #-}+string :: Parser Reply+string = String <$> line++{-# INLINE error #-}+error :: Parser Reply+error = Error <$> line++{-# INLINE integer #-}+integer :: Parser Reply+integer = Integer <$> integral++{-# INLINE bulk #-}+bulk :: Parser Reply+bulk = Bulk <$> do+  len <- integral+  if len < 0+    then return Nothing+    else Just <$> Atto.take len <* eol++-- don't inline it to break the circle between reply and multi+{-# NOINLINE multi #-}+multi :: Parser Reply+multi = Multi <$> do+  len <- integral+  if len < 0+    then return Nothing+    else Just <$> Atto.count len reply++{-# INLINE integral #-}+integral :: Integral i => Parser i+integral = Atto.signed Atto.decimal <* eol++{-# INLINE line #-}+line :: Parser ByteString+line = Atto.takeTill (== 13) <* eol++{-# INLINE eol #-}+eol :: Parser ()+eol = void $ Atto.string "\r\n"
+ examples/Redis/Reply.hs view
@@ -0,0 +1,16 @@++module Redis.Reply+( Reply (..)+)+where++import Data.Int+import Data.ByteString (ByteString)++data Reply+  = String ByteString+  | Error ByteString+  | Integer Int64+  | Bulk (Maybe ByteString)+  | Multi (Maybe [Reply])+  deriving (Show, Eq)
+ examples/Redis/Scanner.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE OverloadedStrings #-}++module Redis.Scanner+( reply+)+where++import Scanner (Scanner)+import qualified Scanner++import Redis.Reply++import Prelude hiding (error)+import Data.ByteString (ByteString)+import qualified Data.Text.Encoding as Text+import qualified Data.Text.Read as Text+import Control.Monad++{-# INLINE reply #-}+reply :: Scanner Reply+reply = do+  c <- Scanner.anyChar8+  case c of+    '+' -> string+    '-' -> error+    ':' -> integer+    '$' -> bulk+    '*' -> multi+    _ -> fail "Unknown reply type"++{-# INLINE string #-}+string :: Scanner Reply+string = String <$> line++{-# INLINE error #-}+error :: Scanner Reply+error = Error <$> line++{-# INLINE integer #-}+integer :: Scanner Reply+integer = Integer <$> integral++{-# INLINE bulk #-}+bulk :: Scanner Reply+bulk = Bulk <$> do+  len <- integral+  if len < 0+    then return Nothing+    else Just <$> Scanner.take len <* eol++-- don't inline it to break the circle between reply and multi+{-# NOINLINE multi #-}+multi :: Scanner Reply+multi = Multi <$> do+  len <- integral+  if len < 0+    then return Nothing+    else Just <$> replicateM len reply++{-# INLINE integral #-}+integral :: Integral i => Scanner i+integral = do+  str <- line+  case Text.signed Text.decimal (Text.decodeUtf8 str) of+    Left err -> fail (show err)+    Right (l, _) -> return l++{-# INLINE line #-}+line :: Scanner ByteString+line = Scanner.takeWhileChar8 (/= '\r') <* eol++{-# INLINE eol #-}+eol :: Scanner ()+eol = do+  Scanner.char8 '\r'+  Scanner.char8 '\n'
+ lib/Scanner.hs view
@@ -0,0 +1,116 @@++-- | Fast not-backtracking incremental scanner for bytestrings+--+-- Unlike attoparsec or most of other parser combinator libraries,+-- scanner doesn't support backtracking. But you probably don't need it+-- anyway, at least if you need fast parser.+--+-- Scanner processes input incrementally. When more input is needed, scanner+-- returns `More` continuation. All the already processed input is discarded.++module Scanner+( Scanner+, Result (..)+, scan+, scanOnly+, scanLazy+, anyWord8+, anyChar8+, word8+, char8+, take+, takeWhile+, takeWhileChar8+, string+, skipWhile+, skipSpace+, lookAhead+, lookAheadChar8+)+where++import Scanner.Internal++import Prelude hiding (take, takeWhile)+import Data.Word+import qualified Data.Char as Char+import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Lazy as Lazy (ByteString)+import qualified Data.ByteString.Lazy as Lazy.ByteString+import Control.Monad+import GHC.Base (unsafeChr)++-- | Scan the complete input, without resupplying+scanOnly :: Scanner a -> ByteString -> Either String a+scanOnly s bs = go (scan s bs)+  where+  go res = case res of+    Done _ r -> Right r+    Fail _ err -> Left err+    More more -> go (more ByteString.empty)++-- | Scan lazy bytestring by resupplying scanner with chunks+scanLazy :: Scanner a -> Lazy.ByteString -> Either String a+scanLazy s lbs = go (scan s) (Lazy.ByteString.toChunks lbs)+  where+  go more chunks =+    let (chunk, chunks') = case chunks of+          [] -> (ByteString.empty, [])+          (c:cs) -> (c, cs)+    in case more chunk of+      Done _ r -> Right r+      Fail _ err -> Left err+      More more' -> go more' chunks'++-- | Consume the next 8-bit char+--+-- It fails if end of input+{-# INLINE anyChar8 #-}+anyChar8 :: Scanner Char+anyChar8 = w2c <$> anyWord8++-- | Consume the specified word or fail+{-# INLINE word8 #-}+word8 :: Word8 -> Scanner ()+word8 w = do+  w' <- anyWord8+  unless (w' == w) $+    fail "unexpected word"++-- | Consume the specified 8-bit char or fail+{-# INLINE char8 #-}+char8 :: Char -> Scanner ()+char8 = word8 . c2w++-- | Take input while the predicate is `True`+{-# INLINE takeWhileChar8 #-}+takeWhileChar8 :: (Char -> Bool) -> Scanner ByteString+takeWhileChar8 p = takeWhile (p . w2c)++-- | Return the next byte, if any, without consuming it+{-# INLINE lookAheadChar8 #-}+lookAheadChar8 :: Scanner (Maybe Char)+lookAheadChar8 = fmap w2c <$> lookAhead++-- | Skip any input while the preducate is `True`+{-# INLINE skipWhile #-}+skipWhile :: (Word8 -> Bool) -> Scanner ()+skipWhile = void . takeWhile++-- | Skip space+{-# INLINE skipSpace #-}+skipSpace :: Scanner ()+skipSpace = skipWhile isSpaceWord8++{-# INLINE isSpaceWord8 #-}+isSpaceWord8 :: Word8 -> Bool+isSpaceWord8 w = w == 32 || w <= 13++{-# INLINE w2c #-}+w2c :: Word8 -> Char+w2c = unsafeChr . fromIntegral++{-# INLINE c2w #-}+c2w :: Char -> Word8+c2w = fromIntegral . Char.ord
+ lib/Scanner/Internal.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_HADDOCK not-home #-}++-- | Scanner implementation++module Scanner.Internal+where++import Prelude hiding (takeWhile)+import Data.Word+import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import Control.Monad++-- | CPS scanner without backtracking+data Scanner a = Scanner+  { run :: forall r. ByteString -> Next a r -> Result r+  }++-- | Scanner continuation+type Next a r = ByteString -> a -> Result r++-- | Scanner result+data Result r+  -- | Successful result with the rest of input+  = Done ByteString r++  -- | Scanner failed with rest of input and error message+  | Fail ByteString String++  -- | Need more input+  | More (ByteString -> Result r)++-- | Run scanner with the input+scan :: Scanner r -> ByteString -> Result r+scan s bs = run s bs Done++instance Functor Scanner where+  {-# INLINE fmap #-}+  fmap f (Scanner s) = Scanner $ \bs next ->+    s bs $ \bs' a ->+      next bs' (f a)++instance Applicative Scanner where+  {-# INLINE pure #-}+  pure = return+  {-# INLINE (<*>) #-}+  (<*>) = ap++  {-# INLINE (*>) #-}+  (*>) = (>>)++  {-# INLINE (<*) #-}+  s1 <* s2 = s1 >>= \a -> s2 >> return a++instance Monad Scanner where+  {-# INLINE return #-}+  return a = Scanner $ \bs next ->+    next bs a++  {-# INLINE (>>=) #-}+  s1 >>= s2 = Scanner $ \bs next ->+    run s1 bs $ \bs' a ->+      run (s2 a) bs' next++  {-# INLINE  fail #-}+  fail err = Scanner $ \bs _ ->+    Fail bs err++-- | Consume the next word+--+-- It fails if end of input+{-# INLINE anyWord8 #-}+anyWord8 :: Scanner Word8+anyWord8 = Scanner $ \bs next ->+  case ByteString.uncons bs of+    Just (c, bs') -> next bs' c+    _ -> More $ \bs' -> slowPath bs' next+  where+  slowPath bs next =+    case ByteString.uncons bs of+      Just (c, bs') -> next bs' c+      _ -> Fail ByteString.empty "No more input"++-- | Take input while the predicate is `True`+{-# INLINE takeWhile #-}+takeWhile :: (Word8 -> Bool) -> Scanner ByteString+takeWhile p = Scanner $ \bs next ->+  let (l, r) = ByteString.span p bs+  in if ByteString.null r+    then More $ \bs' ->+      if ByteString.null bs'+        then next ByteString.empty l+        else run (slowPath l) bs' next+    else next r l+  where+  slowPath l = go [l]+  go res = do+    chunk <- takeChunk+    done <- endOfInput+    if done || ByteString.null chunk+      then return . ByteString.concat . reverse $ (chunk : res)+      else go (chunk : res)+  takeChunk = Scanner $ \bs next ->+    let (l, r) = ByteString.span p bs+    in next r l++-- | Take the specified number of bytes+{-# INLINE take #-}+take :: Int -> Scanner ByteString+take n = Scanner $ \bs next ->+  let len = ByteString.length bs+  in if len >= n+    then let (l, r) = ByteString.splitAt n bs+         in next r l+    else More $ \bs' ->+      if ByteString.null bs'+        then Fail ByteString.empty "No more input"+        else run (slowPath bs len) bs' next+  where+  slowPath bs len = go [bs] (n - len)+  go res 0 = return . ByteString.concat . reverse $ res+  go res i = Scanner $ \bs next ->+    let len = ByteString.length bs+    in if len >= i+      then let (l, r) = ByteString.splitAt i bs+           in next r (ByteString.concat . reverse $ (l : res))+      else More $ \bs' ->+        if ByteString.null bs'+          then Fail ByteString.empty "No more input"+          else run (go (bs : res) (i - len)) bs' next++-- | Returns `True` when there is no more input+{-# INLINE endOfInput #-}+endOfInput :: Scanner Bool+endOfInput = Scanner $ \bs next ->+  if ByteString.null bs+    then More $ \bs' -> next bs' (ByteString.null bs')+    else next bs False++-- | Consume the specified string+--+-- Warning: it is not optimized yet, so for for small string it is better+-- to consume it byte-by-byte using `Scanner.word8`+{-# INLINE string #-}+string :: ByteString -> Scanner ()+string str = Scanner $ \bs next ->+  let bsL = ByteString.length bs+      strL = ByteString.length str+  in if ByteString.isPrefixOf str bs+    then next (ByteString.drop strL bs) ()+    else if ByteString.isPrefixOf bs str+      then More $ \bs' -> run (string (ByteString.drop bsL str)) bs' next+      else Fail bs "unexpected input"++-- | Return the next byte, if any, without consuming it+{-# INLINE lookAhead #-}+lookAhead :: Scanner (Maybe Word8)+lookAhead = Scanner $ \bs next ->+  case ByteString.uncons bs of+    Just (c, _) -> next bs (Just c)+    _ -> More $ \bs' -> slowPath bs' next+  where+  slowPath bs next =+    case ByteString.uncons bs of+      Just (c, _) -> next bs (Just c)+      _ -> next ByteString.empty Nothing
+ scanner.cabal view
@@ -0,0 +1,55 @@+name:                scanner+version:             0.1+synopsis:            Fast non-backtracking incremental combinator parsing for bytestrings+homepage:            https://github.com/Yuras/scanner+license:             BSD3+license-file:        LICENSE+author:              Yuras Shumovich+maintainer:          shumovichy@gmail.com+copyright:           (c) Yuras Shumovich 2016+category:            Parsing+build-type:          Simple+cabal-version:       >=1.10+extra-source-files:  README.md bench/bench.png+description:         Parser combinator library designed to be fast. It doesn't+                     support backtracking.++source-repository head+  type:                git+  location:            git@github.com:Yuras/scanner.git++library+  exposed-modules:     Scanner+                       Scanner.Internal+  other-modules:       Prelude+                       Data.Either+  build-depends:       base <5+                     , bytestring+  hs-source-dirs:      lib, compat+  ghc-options:         -O2+  default-language:    Haskell2010++test-suite spec+  type:                exitcode-stdio-1.0+  hs-source-dirs:      spec, compat+  main-is:             spec.hs+  build-depends:       base+                     , bytestring+                     , hspec+                     , scanner+  default-language:    Haskell2010++benchmark bench+  type:                exitcode-stdio-1.0+  hs-source-dirs:      bench, examples, compat+  main-is:             bench.hs+  other-modules:       Redis.Reply+                       Redis.Atto+                       Redis.Scanner+  default-language:    Haskell2010+  build-depends:       base+                     , bytestring+                     , text+                     , attoparsec+                     , criterion+                     , scanner
+ spec/spec.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}++module Main+( main+)+where++import Scanner++import Prelude hiding (take, takeWhile)+import Data.Either+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Lazy as Lazy.ByteString+import Test.Hspec++main :: IO ()+main = hspec $ do+  anyWord8Spec+  stringSpec+  takeSpec+  takeWhileSpec+  lookAheadSpec++anyWord8Spec :: Spec+anyWord8Spec = describe "anyWord8" $ do+  it "should return the current byte" $ do+    let bs = ByteString.pack [42, 43]+    scanOnly anyWord8 bs `shouldBe` Right 42++  it "should consume the current byte" $ do+    let bs = ByteString.pack [42, 43]+    scanOnly (anyWord8 *> anyWord8) bs `shouldBe` Right 43++    let bs' = Lazy.ByteString.fromChunks+          [ ByteString.pack [42]+          , ByteString.pack [43]+          , ByteString.pack [44]+          ]+    scanLazy (anyWord8 *> anyWord8 *> anyWord8) bs' `shouldBe` Right 44++  it "should ask for more input" $ do+    let bs = Lazy.ByteString.fromChunks+          [ ByteString.pack [42]+          , ByteString.pack [43]+          ]+    scanLazy (anyWord8 *> anyWord8) bs `shouldBe` Right 43++  it "should fail on end of input" $ do+    let bs = ByteString.empty+    scanOnly anyWord8 bs `shouldSatisfy` isLeft++stringSpec :: Spec+stringSpec = describe "string" $ do+  it "should consume the string" $ do+    let bs = "hello world"+    scanOnly (string "hello" *> anyWord8) bs `shouldBe` Right 32++  it "should ask for more input" $ do+    let bs = Lazy.ByteString.fromChunks+          [ "hel"+          , "lo"+          ]+    scanLazy (string "hello") bs `shouldBe` Right ()++  it "should fail on wrong input" $ do+    let bs = "helo world"+    scanOnly (string "hello") bs `shouldSatisfy` isLeft++takeSpec :: Spec+takeSpec = describe "take" $ do+  it "should return the first n bytes" $ do+    let bs = "hello world"+    scanOnly (take 5) bs `shouldBe` Right "hello"++  it "should ask for more input" $ do+    let bs = Lazy.ByteString.fromChunks+          [ "he"+          , "l"+          , "lo world"+          ]+    scanLazy (take 5) bs `shouldBe` Right "hello"++  it "should fail on end of input" $ do+    let bs = "hell"+    scanOnly (take 5) bs `shouldSatisfy` isLeft++    let bs' = Lazy.ByteString.fromChunks+          [ "he"+          , "l"+          , "l"+          ]+    scanLazy (take 5) bs' `shouldSatisfy` isLeft++takeWhileSpec :: Spec+takeWhileSpec = describe "takeWhile" $ do+  it "should return bytes according to the predicate" $ do+    let bs = "hello world"+    scanOnly (takeWhile (/= 32)) bs `shouldBe` Right "hello"++  it "should ask for more input" $ do+    let bs = Lazy.ByteString.fromChunks+          [ "he"+          , "l"+          , "lo world"+          ]+    scanLazy (takeWhile (/= 32)) bs `shouldBe` Right "hello"++  it "should return everything is predicate where becomes False" $ do+    let bs = "hello"+    scanOnly (takeWhile (/= 32)) bs `shouldBe` Right "hello"++lookAheadSpec :: Spec+lookAheadSpec = describe "lookAhead" $ do+  it "should return the next byte" $ do+    let bs = ByteString.pack [42, 43]+    scanOnly lookAhead bs `shouldBe` Right (Just 42)++  it "should return Nothing on end of input" $ do+    let bs = ByteString.empty+    scanOnly lookAhead bs `shouldBe` Right Nothing++  it "should not consume input" $ do+    let bs = ByteString.pack [42, 43]+    scanOnly (lookAhead *> anyWord8) bs `shouldBe` Right 42++  it "should ask for more input" $ do+    let bs = Lazy.ByteString.fromChunks+          [ ByteString.pack [42]+          , ByteString.pack [43]+          ]+    scanLazy (anyWord8 *> lookAhead) bs `shouldBe` Right (Just 43)