diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/bench/bench.hs b/bench/bench.hs
new file mode 100644
--- /dev/null
+++ b/bench/bench.hs
@@ -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"
diff --git a/bench/bench.png b/bench/bench.png
new file mode 100644
Binary files /dev/null and b/bench/bench.png differ
diff --git a/compat/Data/Either.hs b/compat/Data/Either.hs
new file mode 100644
--- /dev/null
+++ b/compat/Data/Either.hs
@@ -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
diff --git a/compat/Prelude.hs b/compat/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/compat/Prelude.hs
@@ -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
diff --git a/examples/Redis/Atto.hs b/examples/Redis/Atto.hs
new file mode 100644
--- /dev/null
+++ b/examples/Redis/Atto.hs
@@ -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"
diff --git a/examples/Redis/Reply.hs b/examples/Redis/Reply.hs
new file mode 100644
--- /dev/null
+++ b/examples/Redis/Reply.hs
@@ -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)
diff --git a/examples/Redis/Scanner.hs b/examples/Redis/Scanner.hs
new file mode 100644
--- /dev/null
+++ b/examples/Redis/Scanner.hs
@@ -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'
diff --git a/lib/Scanner.hs b/lib/Scanner.hs
new file mode 100644
--- /dev/null
+++ b/lib/Scanner.hs
@@ -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
diff --git a/lib/Scanner/Internal.hs b/lib/Scanner/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Scanner/Internal.hs
@@ -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
diff --git a/scanner.cabal b/scanner.cabal
new file mode 100644
--- /dev/null
+++ b/scanner.cabal
@@ -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
diff --git a/spec/spec.hs b/spec/spec.hs
new file mode 100644
--- /dev/null
+++ b/spec/spec.hs
@@ -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)
