diff --git a/Data/Attoparsec/ByteString.hs b/Data/Attoparsec/ByteString.hs
--- a/Data/Attoparsec/ByteString.hs
+++ b/Data/Attoparsec/ByteString.hs
@@ -66,7 +66,9 @@
     , I.runScanner
     , I.takeWhile
     , I.takeWhile1
+    , I.takeWhileIncluding
     , I.takeTill
+    , I.getChunk
 
     -- ** Consume all remaining input
     , I.takeByteString
diff --git a/Data/Attoparsec/ByteString/Buffer.hs b/Data/Attoparsec/ByteString/Buffer.hs
deleted file mode 100644
--- a/Data/Attoparsec/ByteString/Buffer.hs
+++ /dev/null
@@ -1,156 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- |
--- Module      :  Data.Attoparsec.ByteString.Buffer
--- Copyright   :  Bryan O'Sullivan 2007-2015
--- License     :  BSD3
---
--- Maintainer  :  bos@serpentine.com
--- Stability   :  experimental
--- Portability :  GHC
---
--- An "immutable" buffer that supports cheap appends.
---
--- A Buffer is divided into an immutable read-only zone, followed by a
--- mutable area that we've preallocated, but not yet written to.
---
--- We overallocate at the end of a Buffer so that we can cheaply
--- append.  Since a user of an existing Buffer cannot see past the end
--- of its immutable zone into the data that will change during an
--- append, this is safe.
---
--- Once we run out of space at the end of a Buffer, we do the usual
--- doubling of the buffer size.
---
--- The fact of having a mutable buffer really helps with performance,
--- but it does have a consequence: if someone misuses the Partial API
--- that attoparsec uses by calling the same continuation repeatedly
--- (which never makes sense in practice), they could overwrite data.
---
--- Since the API *looks* pure, it should *act* pure, too, so we use
--- two generation counters (one mutable, one immutable) to track the
--- number of appends to a mutable buffer. If the counters ever get out
--- of sync, someone is appending twice to a mutable buffer, so we
--- duplicate the entire buffer in order to preserve the immutability
--- of its older self.
---
--- While we could go a step further and gain protection against API
--- abuse on a multicore system, by use of an atomic increment
--- instruction to bump the mutable generation counter, that would be
--- very expensive, and feels like it would also be in the realm of the
--- ridiculous.  Clients should never call a continuation more than
--- once; we lack a linear type system that could enforce this; and
--- there's only so far we should go to accommodate broken uses.
-
-module Data.Attoparsec.ByteString.Buffer
-    (
-      Buffer
-    , buffer
-    , unbuffer
-    , pappend
-    , length
-    , unsafeIndex
-    , substring
-    , unsafeDrop
-    ) where
-
-import Control.Exception (assert)
-import Data.ByteString.Internal (ByteString(..), memcpy, nullForeignPtr)
-import Data.Attoparsec.Internal.Fhthagn (inlinePerformIO)
-import Data.List (foldl1')
-import Data.Monoid as Mon (Monoid(..))
-import Data.Semigroup (Semigroup(..))
-import Data.Word (Word8)
-import Foreign.ForeignPtr (ForeignPtr, withForeignPtr)
-import Foreign.Ptr (castPtr, plusPtr)
-import Foreign.Storable (peek, peekByteOff, poke, sizeOf)
-import GHC.ForeignPtr (mallocPlainForeignPtrBytes)
-import Prelude hiding (length)
-
--- If _cap is zero, this buffer is empty.
-data Buffer = Buf {
-      _fp  :: {-# UNPACK #-} !(ForeignPtr Word8)
-    , _off :: {-# UNPACK #-} !Int
-    , _len :: {-# UNPACK #-} !Int
-    , _cap :: {-# UNPACK #-} !Int
-    , _gen :: {-# UNPACK #-} !Int
-    }
-
-instance Show Buffer where
-    showsPrec p = showsPrec p . unbuffer
-
--- | The initial 'Buffer' has no mutable zone, so we can avoid all
--- copies in the (hopefully) common case of no further input being fed
--- to us.
-buffer :: ByteString -> Buffer
-buffer (PS fp off len) = Buf fp off len len 0
-
-unbuffer :: Buffer -> ByteString
-unbuffer (Buf fp off len _ _) = PS fp off len
-
-instance Semigroup Buffer where
-    (Buf _ _ _ 0 _) <> b                    = b
-    a               <> (Buf _ _ _ 0 _)      = a
-    buf             <> (Buf fp off len _ _) = append buf fp off len
-
-instance Monoid Buffer where
-    mempty = Buf nullForeignPtr 0 0 0 0
-
-    mappend = (<>)
-
-    mconcat [] = Mon.mempty
-    mconcat xs = foldl1' mappend xs
-
-pappend :: Buffer -> ByteString -> Buffer
-pappend (Buf _ _ _ 0 _) bs  = buffer bs
-pappend buf (PS fp off len) = append buf fp off len
-
-append :: Buffer -> ForeignPtr a -> Int -> Int -> Buffer
-append (Buf fp0 off0 len0 cap0 gen0) !fp1 !off1 !len1 =
-  inlinePerformIO . withForeignPtr fp0 $ \ptr0 ->
-    withForeignPtr fp1 $ \ptr1 -> do
-      let genSize = sizeOf (0::Int)
-          newlen  = len0 + len1
-      gen <- if gen0 == 0
-             then return 0
-             else peek (castPtr ptr0)
-      if gen == gen0 && newlen <= cap0
-        then do
-          let newgen = gen + 1
-          poke (castPtr ptr0) newgen
-          memcpy (ptr0 `plusPtr` (off0+len0))
-                 (ptr1 `plusPtr` off1)
-                 (fromIntegral len1)
-          return (Buf fp0 off0 newlen cap0 newgen)
-        else do
-          let newcap = newlen * 2
-          fp <- mallocPlainForeignPtrBytes (newcap + genSize)
-          withForeignPtr fp $ \ptr_ -> do
-            let ptr    = ptr_ `plusPtr` genSize
-                newgen = 1
-            poke (castPtr ptr_) newgen
-            memcpy ptr (ptr0 `plusPtr` off0) (fromIntegral len0)
-            memcpy (ptr `plusPtr` len0) (ptr1 `plusPtr` off1)
-                   (fromIntegral len1)
-            return (Buf fp genSize newlen newcap newgen)
-
-length :: Buffer -> Int
-length (Buf _ _ len _ _) = len
-{-# INLINE length #-}
-
-unsafeIndex :: Buffer -> Int -> Word8
-unsafeIndex (Buf fp off len _ _) i = assert (i >= 0 && i < len) .
-    inlinePerformIO . withForeignPtr fp $ flip peekByteOff (off+i)
-{-# INLINE unsafeIndex #-}
-
-substring :: Int -> Int -> Buffer -> ByteString
-substring s l (Buf fp off len _ _) =
-  assert (s >= 0 && s <= len) .
-  assert (l >= 0 && l <= len-s) $
-  PS fp (off+s) l
-{-# INLINE substring #-}
-
-unsafeDrop :: Int -> Buffer -> ByteString
-unsafeDrop s (Buf fp off len _ _) =
-  assert (s >= 0 && s <= len) $
-  PS fp (off+s) (len-s)
-{-# INLINE unsafeDrop #-}
diff --git a/Data/Attoparsec/ByteString/Char8.hs b/Data/Attoparsec/ByteString/Char8.hs
--- a/Data/Attoparsec/ByteString/Char8.hs
+++ b/Data/Attoparsec/ByteString/Char8.hs
@@ -489,30 +489,39 @@
 {-# SPECIALIZE rational :: Parser Scientific #-}
 rational = scientifically realToFrac
 
--- | Parse a rational number.
+-- | Parse a 'Double'.
 --
 -- This parser accepts an optional leading sign character, followed by
--- at least one decimal digit.  The syntax similar to that accepted by
--- the 'read' function, with the exception that a trailing @\'.\'@ or
--- @\'e\'@ /not/ followed by a number is not consumed.
+-- at most one decimal digit.  The syntax is similar to that accepted by
+-- the 'read' function, with the exception that a trailing @\'.\'@ is
+-- consumed.
 --
+-- === Examples
+--
+-- These examples use this helper:
+--
+-- @
+-- r :: 'Parser' a -> 'Data.ByteString.ByteString' -> 'Data.Attoparsec.ByteString.Result' a
+-- r p s = 'feed' ('Data.Attoparsec.parse' p s) 'mempty'
+-- @
+--
 -- Examples with behaviour identical to 'read', if you feed an empty
 -- continuation to the first result:
 --
--- >rational "3"     == Done 3.0 ""
--- >rational "3.1"   == Done 3.1 ""
--- >rational "3e4"   == Done 30000.0 ""
--- >rational "3.1e4" == Done 31000.0, ""
+-- > double "3"     == Done "" 3.0
+-- > double "3.1"   == Done "" 3.1
+-- > double "3e4"   == Done "" 30000.0
+-- > double "3.1e4" == Done "" 31000.0
+-- > double "3e"    == Done "e" 3.0
 --
 -- Examples with behaviour identical to 'read':
 --
--- >rational ".3"    == Fail "input does not start with a digit"
--- >rational "e3"    == Fail "input does not start with a digit"
+-- > double ".3"    == Fail ".3" _ _
+-- > double "e3"    == Fail "e3" _ _
 --
--- Examples of differences from 'read':
+-- Example of difference from 'read':
 --
--- >rational "3.foo" == Done 3.0 ".foo"
--- >rational "3e"    == Done 3.0 "e"
+-- > double "3.foo" == Done "foo" 3.0
 --
 -- This function does not accept string representations of \"NaN\" or
 -- \"Infinity\".
diff --git a/Data/Attoparsec/ByteString/FastSet.hs b/Data/Attoparsec/ByteString/FastSet.hs
deleted file mode 100644
--- a/Data/Attoparsec/ByteString/FastSet.hs
+++ /dev/null
@@ -1,115 +0,0 @@
-{-# LANGUAGE BangPatterns, MagicHash #-}
-
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Attoparsec.ByteString.FastSet
--- Copyright   :  Bryan O'Sullivan 2007-2015
--- License     :  BSD3
---
--- Maintainer  :  bos@serpentine.com
--- Stability   :  experimental
--- Portability :  unknown
---
--- Fast set membership tests for 'Word8' and 8-bit 'Char' values.  The
--- set representation is unboxed for efficiency.  For small sets, we
--- test for membership using a binary search.  For larger sets, we use
--- a lookup table.
---
------------------------------------------------------------------------------
-module Data.Attoparsec.ByteString.FastSet
-    (
-    -- * Data type
-      FastSet
-    -- * Construction
-    , fromList
-    , set
-    -- * Lookup
-    , memberChar
-    , memberWord8
-    -- * Debugging
-    , fromSet
-    -- * Handy interface
-    , charClass
-    ) where
-
-import Data.Bits ((.&.), (.|.))
-import Foreign.Storable (peekByteOff, pokeByteOff)
-import GHC.Base (Int(I#), iShiftRA#, narrow8Word#, shiftL#)
-import GHC.Word (Word8(W8#))
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Char8 as B8
-import qualified Data.ByteString.Internal as I
-import qualified Data.ByteString.Unsafe as U
-
-data FastSet = Sorted { fromSet :: !B.ByteString }
-             | Table  { fromSet :: !B.ByteString }
-    deriving (Eq, Ord)
-
-instance Show FastSet where
-    show (Sorted s) = "FastSet Sorted " ++ show (B8.unpack s)
-    show (Table _) = "FastSet Table"
-
--- | The lower bound on the size of a lookup table.  We choose this to
--- balance table density against performance.
-tableCutoff :: Int
-tableCutoff = 8
-
--- | Create a set.
-set :: B.ByteString -> FastSet
-set s | B.length s < tableCutoff = Sorted . B.sort $ s
-      | otherwise                = Table . mkTable $ s
-
-fromList :: [Word8] -> FastSet
-fromList = set . B.pack
-
-data I = I {-# UNPACK #-} !Int {-# UNPACK #-} !Word8
-
-shiftR :: Int -> Int -> Int
-shiftR (I# x#) (I# i#) = I# (x# `iShiftRA#` i#)
-
-shiftL :: Word8 -> Int -> Word8
-shiftL (W8# x#) (I# i#) = W8# (narrow8Word# (x# `shiftL#` i#))
-
-index :: Int -> I
-index i = I (i `shiftR` 3) (1 `shiftL` (i .&. 7))
-{-# INLINE index #-}
-
--- | Check the set for membership.
-memberWord8 :: Word8 -> FastSet -> Bool
-memberWord8 w (Table t)  =
-    let I byte bit = index (fromIntegral w)
-    in  U.unsafeIndex t byte .&. bit /= 0
-memberWord8 w (Sorted s) = search 0 (B.length s - 1)
-    where search lo hi
-              | hi < lo = False
-              | otherwise =
-                  let mid = (lo + hi) `quot` 2
-                  in case compare w (U.unsafeIndex s mid) of
-                       GT -> search (mid + 1) hi
-                       LT -> search lo (mid - 1)
-                       _ -> True
-
--- | Check the set for membership.  Only works with 8-bit characters:
--- characters above code point 255 will give wrong answers.
-memberChar :: Char -> FastSet -> Bool
-memberChar c = memberWord8 (I.c2w c)
-{-# INLINE memberChar #-}
-
-mkTable :: B.ByteString -> B.ByteString
-mkTable s = I.unsafeCreate 32 $ \t -> do
-            _ <- I.memset t 0 32
-            U.unsafeUseAsCStringLen s $ \(p, l) ->
-              let loop n | n == l = return ()
-                         | otherwise = do
-                    c <- peekByteOff p n :: IO Word8
-                    let I byte bit = index (fromIntegral c)
-                    prev <- peekByteOff t byte :: IO Word8
-                    pokeByteOff t byte (prev .|. bit)
-                    loop (n + 1)
-              in loop 0
-
-charClass :: String -> FastSet
-charClass = set . B8.pack . go
-    where go (a:'-':b:xs) = [a..b] ++ go xs
-          go (x:xs) = x : go xs
-          go _ = ""
diff --git a/Data/Attoparsec/ByteString/Internal.hs b/Data/Attoparsec/ByteString/Internal.hs
--- a/Data/Attoparsec/ByteString/Internal.hs
+++ b/Data/Attoparsec/ByteString/Internal.hs
@@ -53,7 +53,9 @@
     , runScanner
     , takeWhile
     , takeWhile1
+    , takeWhileIncluding
     , takeTill
+    , getChunk
 
     -- ** Consume all remaining input
     , takeByteString
@@ -75,6 +77,7 @@
 import Data.Attoparsec.ByteString.FastSet (charClass, memberWord8)
 import Data.Attoparsec.Combinator ((<?>))
 import Data.Attoparsec.Internal
+import Data.Attoparsec.Internal.Compat
 import Data.Attoparsec.Internal.Fhthagn (inlinePerformIO)
 import Data.Attoparsec.Internal.Types hiding (Parser, Failure, Success)
 import Data.ByteString (ByteString)
@@ -276,6 +279,46 @@
       else return $ concatReverse (s:acc)
 {-# INLINE takeWhileAcc #-}
 
+-- | Consume input until immediately after the predicate returns 'True', and return
+-- the consumed input.
+--
+-- This parser will consume at least one 'Word8' or fail.
+takeWhileIncluding :: (Word8 -> Bool) -> Parser B.ByteString
+takeWhileIncluding p = do
+  (s', t) <- B8.span p <$> get
+  case B8.uncons t of
+    -- Since we reached a break point and managed to get the next byte,
+    -- input can not have been exhausted thus we succed and advance unconditionally.
+    Just (h, _) -> do
+      let s = s' `B8.snoc` h
+      advance (B8.length s)
+      return s
+    -- The above isn't true so either we ran out of input or we need to process the next chunk.
+    Nothing -> do
+      continue <- inputSpansChunks (B8.length s')
+      if continue
+        then takeWhileIncAcc p [s']
+        -- Our spec says that if we run out of input we fail.
+        else fail "takeWhileIncluding reached end of input"
+{-# INLINE takeWhileIncluding #-}
+
+takeWhileIncAcc :: (Word8 -> Bool) -> [B.ByteString] -> Parser B.ByteString
+takeWhileIncAcc p = go
+ where
+   go acc = do
+     (s', t) <- B8.span p <$> get
+     case B8.uncons t of
+       Just (h, _) -> do
+         let s = s' `B8.snoc` h
+         advance (B8.length s)
+         return (concatReverse $ s:acc)
+       Nothing -> do
+         continue <- inputSpansChunks (B8.length s')
+         if continue
+           then go (s':acc)
+           else fail "takeWhileIncAcc reached end of input"
+{-# INLINE takeWhileIncAcc #-}
+
 takeRest :: Parser [ByteString]
 takeRest = go []
  where
@@ -296,6 +339,17 @@
 takeLazyByteString :: Parser L.ByteString
 takeLazyByteString = L.fromChunks `fmap` takeRest
 
+-- | Return the rest of the current chunk without consuming anything.
+--
+-- If the current chunk is empty, then ask for more input.
+-- If there is no more input, then return 'Nothing'
+getChunk :: Parser (Maybe ByteString)
+getChunk = do
+  input <- wantInput
+  if input
+    then Just <$> get
+    else return Nothing
+
 data T s = T {-# UNPACK #-} !Int s
 
 scan_ :: (s -> [ByteString] -> Parser r) -> s -> (s -> Word8 -> Maybe s)
@@ -303,7 +357,7 @@
 scan_ f s0 p = go [] s0
  where
   go acc s1 = do
-    let scanner (B.PS fp off len) =
+    let scanner bs = withPS bs $ \fp off len ->
           withForeignPtr fp $ \ptr0 -> do
             let start = ptr0 `plusPtr` off
                 end   = start `plusPtr` len
diff --git a/Data/Attoparsec/ByteString/Lazy.hs b/Data/Attoparsec/ByteString/Lazy.hs
--- a/Data/Attoparsec/ByteString/Lazy.hs
+++ b/Data/Attoparsec/ByteString/Lazy.hs
@@ -33,6 +33,7 @@
     , module Data.Attoparsec.ByteString
     -- * Running parsers
     , parse
+    , parseOnly
     , parseTest
     -- ** Result conversion
     , maybeResult
@@ -47,7 +48,7 @@
 import qualified Data.Attoparsec.Internal.Types as T
 import Data.Attoparsec.ByteString
     hiding (IResult(..), Result, eitherResult, maybeResult,
-            parse, parseWith, parseTest)
+            parse, parseOnly, parseWith, parseTest)
 
 -- | The result of a parse.
 data Result r = Fail ByteString [String] String
@@ -108,3 +109,16 @@
 eitherResult (Done _ r)        = Right r
 eitherResult (Fail _ [] msg)   = Left msg
 eitherResult (Fail _ ctxs msg) = Left (intercalate " > " ctxs ++ ": " ++ msg)
+
+-- | Run a parser that cannot be resupplied via a 'T.Partial' result.
+--
+-- This function does not force a parser to consume all of its input.
+-- Instead, any residual input will be discarded.  To force a parser
+-- to consume all of its input, use something like this:
+--
+-- @
+--'parseOnly' (myParser 'Control.Applicative.<*' 'endOfInput')
+-- @
+parseOnly :: A.Parser a -> ByteString -> Either String a
+parseOnly p = eitherResult . parse p
+{-# INLINE parseOnly #-}
diff --git a/Data/Attoparsec/Combinator.hs b/Data/Attoparsec/Combinator.hs
--- a/Data/Attoparsec/Combinator.hs
+++ b/Data/Attoparsec/Combinator.hs
@@ -43,11 +43,12 @@
 import Control.Applicative (Applicative(..), (<$>))
 import Data.Monoid (Monoid(mappend))
 #endif
-import Control.Applicative (Alternative(..), empty, liftA2, many, (<|>))
+import Control.Applicative (Alternative(..), liftA2, many, (<|>))
 import Control.Monad (MonadPlus(..))
 import Data.Attoparsec.Internal.Types (Parser(..), IResult(..))
 import Data.Attoparsec.Internal (endOfInput, atEnd, satisfyElem)
 import Data.ByteString (ByteString)
+import Data.Foldable (asum)
 import Data.Text (Text)
 import qualified Data.Attoparsec.Zepto as Z
 import Prelude hiding (succ)
@@ -58,7 +59,7 @@
 -- This combinator is provided for compatibility with Parsec.
 -- attoparsec parsers always backtrack on failure.
 try :: Parser i a -> Parser i a
-try p = p
+try = id
 {-# INLINE try #-}
 
 -- | Name the parser, in case failure occurs.
@@ -75,7 +76,7 @@
 -- until one of them succeeds. Returns the value of the succeeding
 -- action.
 choice :: Alternative f => [f a] -> f a
-choice = foldr (<|>) empty
+choice = asum
 {-# SPECIALIZE choice :: [Parser ByteString a]
                       -> Parser ByteString a #-}
 {-# SPECIALIZE choice :: [Parser Text a] -> Parser Text a #-}
diff --git a/Data/Attoparsec/Internal/Fhthagn.hs b/Data/Attoparsec/Internal/Fhthagn.hs
deleted file mode 100644
--- a/Data/Attoparsec/Internal/Fhthagn.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-{-# LANGUAGE BangPatterns, Rank2Types, OverloadedStrings,
-    RecordWildCards, MagicHash, UnboxedTuples #-}
-
-module Data.Attoparsec.Internal.Fhthagn
-    (
-      inlinePerformIO
-    ) where
-
-import GHC.Base (realWorld#)
-import GHC.IO (IO(IO))
-
--- | Just like unsafePerformIO, but we inline it. Big performance gains as
--- it exposes lots of things to further inlining. /Very unsafe/. In
--- particular, you should do no memory allocation inside an
--- 'inlinePerformIO' block. On Hugs this is just @unsafePerformIO@.
-inlinePerformIO :: IO a -> a
-inlinePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
-{-# INLINE inlinePerformIO #-}
diff --git a/Data/Attoparsec/Internal/Types.hs b/Data/Attoparsec/Internal/Types.hs
--- a/Data/Attoparsec/Internal/Types.hs
+++ b/Data/Attoparsec/Internal/Types.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE BangPatterns, GeneralizedNewtypeDeriving, OverloadedStrings,
+{-# LANGUAGE CPP, BangPatterns, GeneralizedNewtypeDeriving, OverloadedStrings,
     Rank2Types, RecordWildCards, TypeFamilies #-}
 -- |
 -- Module      :  Data.Attoparsec.Internal.Types
@@ -39,7 +39,7 @@
 import Data.Text (Text)
 import qualified Data.Text as Text
 import Data.Text.Unsafe (Iter(..))
-import Prelude hiding (getChar, succ)
+import Prelude hiding (succ)
 import qualified Data.Attoparsec.ByteString.Buffer as B
 import qualified Data.Attoparsec.Text.Buffer as T
 
@@ -136,8 +136,10 @@
     mempty  = Incomplete
 
 instance Monad (Parser i) where
+#if !(MIN_VERSION_base(4,13,0))
     fail = Fail.fail
     {-# INLINE fail #-}
+#endif
 
     return = App.pure
     {-# INLINE return #-}
@@ -180,7 +182,7 @@
 {-# INLINE apP #-}
 
 instance Applicative (Parser i) where
-    pure v = Parser $ \t pos more _lose succ -> succ t pos more v
+    pure v = Parser $ \t !pos more _lose succ -> succ t pos more v
     {-# INLINE pure #-}
     (<*>)  = apP
     {-# INLINE (<*>) #-}
@@ -207,8 +209,9 @@
     {-# INLINE (<|>) #-}
 
     many v = many_v
-        where many_v = some_v <|> pure []
-              some_v = (:) App.<$> v <*> many_v
+      where
+        many_v = some_v <|> pure []
+        some_v = (:) <$> v <*> many_v
     {-# INLINE many #-}
 
     some v = some_v
diff --git a/Data/Attoparsec/Text.hs b/Data/Attoparsec/Text.hs
--- a/Data/Attoparsec/Text.hs
+++ b/Data/Attoparsec/Text.hs
@@ -360,30 +360,39 @@
 {-# SPECIALIZE rational :: Parser Scientific #-}
 rational = scientifically realToFrac
 
--- | Parse a rational number.
+-- | Parse a 'Double'.
 --
 -- This parser accepts an optional leading sign character, followed by
--- at least one decimal digit.  The syntax similar to that accepted by
--- the 'read' function, with the exception that a trailing @\'.\'@ or
--- @\'e\'@ /not/ followed by a number is not consumed.
+-- at most one decimal digit.  The syntax is similar to that accepted by
+-- the 'read' function, with the exception that a trailing @\'.\'@ is
+-- consumed.
 --
+-- === Examples
+--
+-- These examples use this helper:
+--
+-- @
+-- r :: 'Parser' a -> 'Data.Text.Text' -> 'Data.Attoparsec.Text.Result' a
+-- r p s = 'feed' ('Data.Attoparsec.parse' p s) 'mempty'
+-- @
+--
 -- Examples with behaviour identical to 'read', if you feed an empty
 -- continuation to the first result:
 --
--- >rational "3"     == Done 3.0 ""
--- >rational "3.1"   == Done 3.1 ""
--- >rational "3e4"   == Done 30000.0 ""
--- >rational "3.1e4" == Done 31000.0, ""
+-- > r double "3"     == Done "" 3.0
+-- > r double "3.1"   == Done "" 3.1
+-- > r double "3e4"   == Done "" 30000.0
+-- > r double "3.1e4" == Done "" 31000.0
+-- > r double "3e"    == Done "e" 3.0
 --
 -- Examples with behaviour identical to 'read':
 --
--- >rational ".3"    == Fail "input does not start with a digit"
--- >rational "e3"    == Fail "input does not start with a digit"
+-- > r double ".3"    == Fail ".3" _ _
+-- > r double "e3"    == Fail "e3" _ _
 --
--- Examples of differences from 'read':
+-- Example of difference from 'read':
 --
--- >rational "3.foo" == Done 3.0 ".foo"
--- >rational "3e"    == Done 3.0 "e"
+-- > r double "3.foo" == Done "foo" 3.0
 --
 -- This function does not accept string representations of \"NaN\" or
 -- \"Infinity\".
diff --git a/Data/Attoparsec/Text/Buffer.hs b/Data/Attoparsec/Text/Buffer.hs
deleted file mode 100644
--- a/Data/Attoparsec/Text/Buffer.hs
+++ /dev/null
@@ -1,172 +0,0 @@
-{-# LANGUAGE BangPatterns, CPP, MagicHash, RankNTypes, RecordWildCards,
-    UnboxedTuples #-}
-
--- |
--- Module      :  Data.Attoparsec.Text.Buffer
--- Copyright   :  Bryan O'Sullivan 2007-2015
--- License     :  BSD3
---
--- Maintainer  :  bos@serpentine.com
--- Stability   :  experimental
--- Portability :  GHC
---
--- An immutable buffer that supports cheap appends.
-
--- A Buffer is divided into an immutable read-only zone, followed by a
--- mutable area that we've preallocated, but not yet written to.
---
--- We overallocate at the end of a Buffer so that we can cheaply
--- append.  Since a user of an existing Buffer cannot see past the end
--- of its immutable zone into the data that will change during an
--- append, this is safe.
---
--- Once we run out of space at the end of a Buffer, we do the usual
--- doubling of the buffer size.
-
-module Data.Attoparsec.Text.Buffer
-    (
-      Buffer
-    , buffer
-    , unbuffer
-    , unbufferAt
-    , length
-    , pappend
-    , iter
-    , iter_
-    , substring
-    , dropWord16
-    ) where
-
-import Control.Exception (assert)
-import Data.Bits (shiftR)
-import Data.List (foldl1')
-import Data.Monoid as Mon (Monoid(..))
-import Data.Semigroup (Semigroup(..))
-import Data.Text ()
-import Data.Text.Internal (Text(..))
-import Data.Text.Internal.Encoding.Utf16 (chr2)
-import Data.Text.Internal.Unsafe.Char (unsafeChr)
-import Data.Text.Unsafe (Iter(..))
-import Foreign.Storable (sizeOf)
-import GHC.Base (Int(..), indexIntArray#, unsafeCoerce#, writeIntArray#)
-import GHC.ST (ST(..), runST)
-import Prelude hiding (length)
-import qualified Data.Text.Array as A
-
--- If _cap is zero, this buffer is empty.
-data Buffer = Buf {
-      _arr :: {-# UNPACK #-} !A.Array
-    , _off :: {-# UNPACK #-} !Int
-    , _len :: {-# UNPACK #-} !Int
-    , _cap :: {-# UNPACK #-} !Int
-    , _gen :: {-# UNPACK #-} !Int
-    }
-
-instance Show Buffer where
-    showsPrec p = showsPrec p . unbuffer
-
--- | The initial 'Buffer' has no mutable zone, so we can avoid all
--- copies in the (hopefully) common case of no further input being fed
--- to us.
-buffer :: Text -> Buffer
-buffer (Text arr off len) = Buf arr off len len 0
-
-unbuffer :: Buffer -> Text
-unbuffer (Buf arr off len _ _) = Text arr off len
-
-unbufferAt :: Int -> Buffer -> Text
-unbufferAt s (Buf arr off len _ _) =
-  assert (s >= 0 && s <= len) $
-  Text arr (off+s) (len-s)
-
-instance Semigroup Buffer where
-    (Buf _ _ _ 0 _) <> b                     = b
-    a               <> (Buf _ _ _ 0 _)       = a
-    buf             <> (Buf arr off len _ _) = append buf arr off len
-    {-# INLINE (<>) #-}
-
-instance Monoid Buffer where
-    mempty = Buf A.empty 0 0 0 0
-    {-# INLINE mempty #-}
-
-    mappend = (<>)
-
-    mconcat [] = Mon.mempty
-    mconcat xs = foldl1' (<>) xs
-
-pappend :: Buffer -> Text -> Buffer
-pappend (Buf _ _ _ 0 _) t      = buffer t
-pappend buf (Text arr off len) = append buf arr off len
-
-append :: Buffer -> A.Array -> Int -> Int -> Buffer
-append (Buf arr0 off0 len0 cap0 gen0) !arr1 !off1 !len1 = runST $ do
-  let woff    = sizeOf (0::Int) `shiftR` 1
-      newlen  = len0 + len1
-      !gen    = if gen0 == 0 then 0 else readGen arr0
-  if gen == gen0 && newlen <= cap0
-    then do
-      let newgen = gen + 1
-      marr <- unsafeThaw arr0
-      writeGen marr newgen
-      A.copyI marr (off0+len0) arr1 off1 (off0+newlen)
-      arr2 <- A.unsafeFreeze marr
-      return (Buf arr2 off0 newlen cap0 newgen)
-    else do
-      let newcap = newlen * 2
-          newgen = 1
-      marr <- A.new (newcap + woff)
-      writeGen marr newgen
-      A.copyI marr woff arr0 off0 (woff+len0)
-      A.copyI marr (woff+len0) arr1 off1 (woff+newlen)
-      arr2 <- A.unsafeFreeze marr
-      return (Buf arr2 woff newlen newcap newgen)
-
-length :: Buffer -> Int
-length (Buf _ _ len _ _) = len
-{-# INLINE length #-}
-
-substring :: Int -> Int -> Buffer -> Text
-substring s l (Buf arr off len _ _) =
-  assert (s >= 0 && s <= len) .
-  assert (l >= 0 && l <= len-s) $
-  Text arr (off+s) l
-{-# INLINE substring #-}
-
-dropWord16 :: Int -> Buffer -> Text
-dropWord16 s (Buf arr off len _ _) =
-  assert (s >= 0 && s <= len) $
-  Text arr (off+s) (len-s)
-{-# INLINE dropWord16 #-}
-
--- | /O(1)/ Iterate (unsafely) one step forwards through a UTF-16
--- array, returning the current character and the delta to add to give
--- the next offset to iterate at.
-iter :: Buffer -> Int -> Iter
-iter (Buf arr off _ _ _) i
-    | m < 0xD800 || m > 0xDBFF = Iter (unsafeChr m) 1
-    | otherwise                = Iter (chr2 m n) 2
-  where m = A.unsafeIndex arr j
-        n = A.unsafeIndex arr k
-        j = off + i
-        k = j + 1
-{-# INLINE iter #-}
-
--- | /O(1)/ Iterate one step through a UTF-16 array, returning the
--- delta to add to give the next offset to iterate at.
-iter_ :: Buffer -> Int -> Int
-iter_ (Buf arr off _ _ _) i | m < 0xD800 || m > 0xDBFF = 1
-                                | otherwise                = 2
-  where m = A.unsafeIndex arr (off+i)
-{-# INLINE iter_ #-}
-
-unsafeThaw :: A.Array -> ST s (A.MArray s)
-unsafeThaw A.Array{..} = ST $ \s# ->
-                          (# s#, A.MArray (unsafeCoerce# aBA) #)
-
-readGen :: A.Array -> Int
-readGen a = case indexIntArray# (A.aBA a) 0# of r# -> I# r#
-
-writeGen :: A.MArray s -> Int -> ST s ()
-writeGen a (I# gen#) = ST $ \s0# ->
-  case writeIntArray# (A.maBA a) 0# gen# s0# of
-    s1# -> (# s1#, () #)
diff --git a/Data/Attoparsec/Text/FastSet.hs b/Data/Attoparsec/Text/FastSet.hs
deleted file mode 100644
--- a/Data/Attoparsec/Text/FastSet.hs
+++ /dev/null
@@ -1,121 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
-------------------------------------------------------------------------------
--- |
--- Module      :  Data.Attoparsec.FastSet
--- Copyright   :  Felipe Lessa 2010, Bryan O'Sullivan 2007-2015
--- License     :  BSD3
---
--- Maintainer  :  felipe.lessa@gmail.com
--- Stability   :  experimental
--- Portability :  unknown
---
--- Fast set membership tests for 'Char' values. We test for
--- membership using a hashtable implemented with Robin Hood
--- collision resolution. The set representation is unboxed,
--- and the characters and hashes interleaved, for efficiency.
---
---
------------------------------------------------------------------------------
-module Data.Attoparsec.Text.FastSet
-    (
-    -- * Data type
-      FastSet
-    -- * Construction
-    , fromList
-    , set
-    -- * Lookup
-    , member
-    -- * Handy interface
-    , charClass
-    ) where
-
-import Data.Bits ((.|.), (.&.), shiftR)
-import Data.Function (on)
-import Data.List (sort, sortBy)
-import qualified Data.Array.Base as AB
-import qualified Data.Array.Unboxed as A
-import qualified Data.Text as T
-
-data FastSet = FastSet {
-    table :: {-# UNPACK #-} !(A.UArray Int Int)
-  , mask  :: {-# UNPACK #-} !Int
-  }
-
-data Entry = Entry {
-    key          :: {-# UNPACK #-} !Char
-  , initialIndex :: {-# UNPACK #-} !Int
-  , index        :: {-# UNPACK #-} !Int
-  }
-
-offset :: Entry -> Int
-offset e = index e - initialIndex e
-
-resolveCollisions :: [Entry] -> [Entry]
-resolveCollisions [] = []
-resolveCollisions [e] = [e]
-resolveCollisions (a:b:entries) = a' : resolveCollisions (b' : entries)
-  where (a', b')
-          | index a < index b   = (a, b)
-          | offset a < offset b = (b { index=index a }, a { index=index a + 1 })
-          | otherwise           = (a, b { index=index a + 1 })
-
-pad :: Int -> [Entry] -> [Entry]
-pad = go 0
-  where -- ensure that we pad enough so that lookups beyond the
-        -- last hash in the table fall within the array
-        go !_ !m []          = replicate (max 1 m + 1) empty
-        go  k  m (e:entries) = map (const empty) [k..i - 1] ++ e :
-                               go (i + 1) (m + i - k - 1) entries
-          where i            = index e
-        empty                = Entry '\0' maxBound 0
-
-nextPowerOf2 :: Int -> Int
-nextPowerOf2 0  = 1
-nextPowerOf2 x  = go (x - 1) 1
-  where go y 32 = y + 1
-        go y k  = go (y .|. (y `shiftR` k)) $ k * 2
-
-fastHash :: Char -> Int
-fastHash = fromEnum
-
-fromList :: String -> FastSet
-fromList s = FastSet (AB.listArray (0, length interleaved - 1) interleaved)
-             mask'
-  where s'      = ordNub (sort s)
-        l       = length s'
-        mask'   = nextPowerOf2 ((5 * l) `div` 4) - 1
-        entries = pad mask' .
-                  resolveCollisions .
-                  sortBy (compare `on` initialIndex) .
-                  zipWith (\c i -> Entry c i i) s' .
-                  map ((.&. mask') . fastHash) $ s'
-        interleaved = concatMap (\e -> [fromEnum $ key e, initialIndex e])
-                      entries
-
-ordNub :: Eq a => [a] -> [a]
-ordNub []     = []
-ordNub (y:ys) = go y ys
-  where go x (z:zs)
-          | x == z    = go x zs
-          | otherwise = x : go z zs
-        go x []       = [x]
-
-set :: T.Text -> FastSet
-set = fromList . T.unpack
-
--- | Check the set for membership.
-member :: Char -> FastSet -> Bool
-member c a           = go (2 * i)
-  where i            = fastHash c .&. mask a
-        lookupAt j b = (i' <= i) && (c == c' || b)
-            where c' = toEnum $ AB.unsafeAt (table a) j
-                  i' = AB.unsafeAt (table a) $ j + 1
-        go j         = lookupAt j . lookupAt (j + 2) . lookupAt (j + 4) .
-                       lookupAt (j + 6) . go $ j + 8
-
-charClass :: String -> FastSet
-charClass = fromList . go
-  where go (a:'-':b:xs) = [a..b] ++ go xs
-        go (x:xs)       = x : go xs
-        go _            = ""
diff --git a/Data/Attoparsec/Text/Internal.hs b/Data/Attoparsec/Text/Internal.hs
--- a/Data/Attoparsec/Text/Internal.hs
+++ b/Data/Attoparsec/Text/Internal.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE BangPatterns, CPP, FlexibleInstances, GADTs, OverloadedStrings,
+{-# LANGUAGE BangPatterns, FlexibleInstances, GADTs, OverloadedStrings,
     Rank2Types, RecordWildCards, TypeFamilies, TypeSynonymInstances #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 -- |
@@ -65,17 +65,14 @@
     , atEnd
     ) where
 
-#if !MIN_VERSION_base(4,8,0)
-import Control.Applicative ((<$>))
-#endif
-import Control.Applicative ((<|>))
+import Control.Applicative ((<|>), (<$>), pure, (*>))
 import Control.Monad (when)
 import Data.Attoparsec.Combinator ((<?>))
 import Data.Attoparsec.Internal
 import Data.Attoparsec.Internal.Types hiding (Parser, Failure, Success)
 import qualified Data.Attoparsec.Text.Buffer as Buf
 import Data.Attoparsec.Text.Buffer (Buffer, buffer)
-import Data.Char (chr, ord)
+import Data.Char (isAsciiUpper, isAsciiLower, toUpper, toLower)
 import Data.List (intercalate)
 import Data.String (IsString(..))
 import Data.Text.Internal (Text(..))
@@ -179,7 +176,7 @@
          | T.null ft         -> suspended s s t pos more lose succ
          | otherwise         -> lose t pos more [] "string"
        Just (pfx,ssfx,tsfx)
-         | T.null ssfx       -> let l = Pos (T.lengthWord16 pfx)
+         | T.null ssfx       -> let l = Pos (Buf.lengthCodeUnits pfx)
                                 in succ t (pos + l) more (substring pos l t)
          | not (T.null tsfx) -> lose t pos more [] "string"
          | otherwise         -> suspended s ssfx t pos more lose succ
@@ -198,7 +195,7 @@
       in case T.commonPrefixes s0 s of
         Nothing         -> lose t pos more [] "string"
         Just (_pfx,ssfx,tsfx)
-          | T.null ssfx -> let l = Pos (T.lengthWord16 s000)
+          | T.null ssfx -> let l = Pos (Buf.lengthCodeUnits s000)
                            in succ t (pos + l) more (substring pos l t)
           | T.null tsfx -> stringSuspended f s000 ssfx t pos more lose succ
           | otherwise   -> lose t pos more [] "string"
@@ -225,15 +222,16 @@
 
 -- | Satisfy a literal string, ignoring case for characters in the ASCII range.
 asciiCI :: Text -> Parser Text
-asciiCI s = string_ (stringSuspended asciiToLower) asciiToLower s
-  where
-    asciiToLower = T.map f
-      where
-        offset = ord 'a' - ord 'A'
-        f c | 'A' <= c && c <= 'Z' = chr (ord c + offset)
-            | otherwise            = c
+asciiCI s = fmap fst $ match $ T.foldr ((*>) . asciiCharCI) (pure ()) s
 {-# INLINE asciiCI #-}
 
+asciiCharCI :: Char -> Parser Char
+asciiCharCI c
+  | isAsciiUpper c = char c <|> char (toLower c)
+  | isAsciiLower c = char c <|> char (toUpper c)
+  | otherwise = char c
+{-# INLINE asciiCharCI #-}
+
 -- | Skip past input for as long as the predicate returns 'True'.
 skipWhile :: (Char -> Bool) -> Parser ()
 skipWhile p = go
@@ -447,12 +445,12 @@
 
 -- | Terminal failure continuation.
 failK :: Failure a
-failK t (Pos pos) _more stack msg = Fail (Buf.dropWord16 pos t) stack msg
+failK t (Pos pos) _more stack msg = Fail (Buf.dropCodeUnits pos t) stack msg
 {-# INLINE failK #-}
 
 -- | Terminal success continuation.
 successK :: Success a a
-successK t (Pos pos) _more a = Done (Buf.dropWord16 pos t) a
+successK t (Pos pos) _more a = Done (Buf.dropCodeUnits pos t) a
 {-# INLINE successK #-}
 
 -- | Run a parser.
@@ -479,7 +477,7 @@
 
 get :: Parser Text
 get = T.Parser $ \t pos more _lose succ ->
-  succ t pos more (Buf.dropWord16 (fromPos pos) t)
+  succ t pos more (Buf.dropCodeUnits (fromPos pos) t)
 {-# INLINE get #-}
 
 endOfChunk :: Parser Bool
diff --git a/Data/Attoparsec/Text/Lazy.hs b/Data/Attoparsec/Text/Lazy.hs
--- a/Data/Attoparsec/Text/Lazy.hs
+++ b/Data/Attoparsec/Text/Lazy.hs
@@ -34,6 +34,7 @@
     , module Data.Attoparsec.Text
     -- * Running parsers
     , parse
+    , parseOnly
     , parseTest
     -- ** Result conversion
     , maybeResult
@@ -47,7 +48,7 @@
 import qualified Data.Attoparsec.Text as A
 import qualified Data.Text as T
 import Data.Attoparsec.Text hiding (IResult(..), Result, eitherResult,
-                                    maybeResult, parse, parseWith, parseTest)
+                                    maybeResult, parse, parseOnly, parseWith, parseTest)
 
 -- | The result of a parse.
 data Result r = Fail Text [String] String
@@ -99,3 +100,16 @@
 eitherResult (Done _ r)        = Right r
 eitherResult (Fail _ [] msg)   = Left msg
 eitherResult (Fail _ ctxs msg) = Left (intercalate " > " ctxs ++ ": " ++ msg)
+
+-- | Run a parser that cannot be resupplied via a 'T.Partial' result.
+--
+-- This function does not force a parser to consume all of its input.
+-- Instead, any residual input will be discarded.  To force a parser
+-- to consume all of its input, use something like this:
+--
+-- @
+--'parseOnly' (myParser 'Control.Applicative.<*' 'endOfInput')
+-- @
+parseOnly :: A.Parser a -> Text -> Either String a
+parseOnly p = eitherResult . parse p
+{-# INLINE parseOnly #-}
diff --git a/Data/Attoparsec/Zepto.hs b/Data/Attoparsec/Zepto.hs
--- a/Data/Attoparsec/Zepto.hs
+++ b/Data/Attoparsec/Zepto.hs
@@ -92,8 +92,10 @@
         Fail err -> return (Fail err)
     {-# INLINE (>>=) #-}
 
+#if !(MIN_VERSION_base(4,13,0))
     fail = Fail.fail
     {-# INLINE fail #-}
+#endif
 
 instance Monad m => Fail.MonadFail (ZeptoT m) where
     fail msg = Parser $ \_ -> return (Fail msg)
diff --git a/attoparsec.cabal b/attoparsec.cabal
--- a/attoparsec.cabal
+++ b/attoparsec.cabal
@@ -1,16 +1,16 @@
 name:            attoparsec
-version:         0.13.1.0
+version:         0.14.4
 license:         BSD3
 license-file:    LICENSE
 category:        Text, Parsing
 author:          Bryan O'Sullivan <bos@serpentine.com>
-maintainer:      Bryan O'Sullivan <bos@serpentine.com>
+maintainer:      Bryan O'Sullivan <bos@serpentine.com>, Ben Gamari <ben@smart-cactus.org>
 stability:       experimental
-tested-with:     GHC == 7.0.1, GHC == 7.2.1, GHC == 7.4.2, GHC ==7.6.3, GHC ==7.8.4, GHC ==7.10.3
+tested-with:     GHC == 7.4.2, GHC ==7.6.3, GHC ==7.8.4, GHC ==7.10.3, GHC ==8.0.2, GHC ==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.7, GHC ==9.0.2, GHC ==9.2.1
 synopsis:        Fast combinator parsing for bytestrings and text
-cabal-version:   >= 1.8
-homepage:        https://github.com/bos/attoparsec
-bug-reports:     https://github.com/bos/attoparsec/issues
+cabal-version:   2.0
+homepage:        https://github.com/bgamari/attoparsec
+bug-reports:     https://github.com/bgamari/attoparsec/issues
 build-type:      Simple
 description:
     A fast parser combinator library, aimed particularly at dealing
@@ -18,8 +18,6 @@
     file formats.
 extra-source-files:
     README.markdown
-    benchmarks/*.cabal
-    benchmarks/*.hs
     benchmarks/*.txt
     benchmarks/json-data/*.json
     benchmarks/Makefile
@@ -28,24 +26,42 @@
     examples/*.c
     examples/*.hs
     examples/Makefile
-    tests/*.hs
-    tests/QC/*.hs
-    tests/QC/IPv6/*.hs
 
 Flag developer
   Description: Whether to build the library in development mode
   Default: False
   Manual: True
 
+-- We need to test and benchmark these modules,
+-- but do not want to expose them to end users
+library attoparsec-internal
+  hs-source-dirs: internal
+  build-depends: array,
+                 base >= 4.3 && < 5,
+                 bytestring <0.12,
+                 text >= 1.1.1.3
+  if !impl(ghc >= 8.0)
+    build-depends: semigroups >=0.16.1 && <0.21
+  exposed-modules: Data.Attoparsec.ByteString.Buffer
+                   Data.Attoparsec.ByteString.FastSet
+                   Data.Attoparsec.Internal.Compat
+                   Data.Attoparsec.Internal.Fhthagn
+                   Data.Attoparsec.Text.Buffer
+                   Data.Attoparsec.Text.FastSet
+  ghc-options: -O2 -Wall
+  default-language: Haskell2010
+
 library
   build-depends: array,
-                 base >= 4.2 && < 5,
-                 bytestring,
+                 base >= 4.3 && < 5,
+                 bytestring <0.12,
                  containers,
                  deepseq,
                  scientific >= 0.3.1 && < 0.4,
-                 transformers,
-                 text >= 1.1.1.3
+                 transformers >= 0.2 && (< 0.4 || >= 0.4.1.0) && < 0.7,
+                 text >= 1.1.1.3,
+                 ghc-prim <0.9,
+                 attoparsec-internal
   if impl(ghc < 7.4)
     build-depends:
       bytestring < 0.10.4.0
@@ -53,7 +69,7 @@
   if !impl(ghc >= 8.0)
     -- Data.Semigroup && Control.Monad.Fail are available in base-4.9+
     build-depends: fail == 4.9.*,
-                   semigroups >=0.16.1 && <0.19
+                   semigroups >=0.16.1 && <0.21
 
   exposed-modules: Data.Attoparsec
                    Data.Attoparsec.ByteString
@@ -69,22 +85,19 @@
                    Data.Attoparsec.Text.Lazy
                    Data.Attoparsec.Types
                    Data.Attoparsec.Zepto
-  other-modules:   Data.Attoparsec.ByteString.Buffer
-                   Data.Attoparsec.ByteString.FastSet
-                   Data.Attoparsec.ByteString.Internal
-                   Data.Attoparsec.Internal.Fhthagn
-                   Data.Attoparsec.Text.Buffer
-                   Data.Attoparsec.Text.FastSet
+  other-modules:   Data.Attoparsec.ByteString.Internal
                    Data.Attoparsec.Text.Internal
   ghc-options: -O2 -Wall
 
+  default-language: Haskell2010
+
   if flag(developer)
     ghc-prof-options: -auto-all
     ghc-options: -Werror
 
-test-suite tests
+test-suite attoparsec-tests
   type:           exitcode-stdio-1.0
-  hs-source-dirs: tests .
+  hs-source-dirs: tests
   main-is:        QC.hs
   other-modules:  QC.Buffer
                   QC.ByteString
@@ -106,10 +119,12 @@
 
   build-depends:
     array,
-    base >= 4 && < 5,
+    attoparsec,
+    attoparsec-internal,
+    base,
     bytestring,
     deepseq >= 1.1,
-    QuickCheck >= 2.7,
+    QuickCheck >= 2.13.2 && < 2.15,
     quickcheck-unicode,
     scientific,
     tasty >= 0.11,
@@ -118,18 +133,22 @@
     transformers,
     vector
 
+  default-language: Haskell2010
+
   if !impl(ghc >= 8.0)
     -- Data.Semigroup && Control.Monad.Fail are available in base-4.9+
     build-depends: fail == 4.9.*,
                    semigroups >=0.16.1 && <0.19
 
-benchmark benchmarks
+benchmark attoparsec-benchmarks
   type: exitcode-stdio-1.0
-  hs-source-dirs: benchmarks benchmarks/warp-3.0.1.1 .
+  hs-source-dirs: benchmarks benchmarks/warp-3.0.1.1
   ghc-options: -O2 -Wall -rtsopts
   main-is: Benchmarks.hs
   other-modules:
+    Aeson
     Common
+    Genome
     HeadersByteString
     HeadersByteString.Atto
     HeadersText
@@ -147,11 +166,12 @@
 
   build-depends:
     array,
+    attoparsec,
+    attoparsec-internal,
     base == 4.*,
     bytestring >= 0.10.4.0,
     case-insensitive,
     containers,
-    criterion >= 1.0,
     deepseq >= 1.1,
     directory,
     filepath,
@@ -159,11 +179,14 @@
     http-types,
     parsec >= 3.1.2,
     scientific,
+    tasty-bench >= 0.3,
     text >= 1.1.1.0,
     transformers,
     unordered-containers,
     vector
 
+  default-language: Haskell2010
+
   if !impl(ghc >= 8.0)
     -- Data.Semigroup && Control.Monad.Fail are available in base-4.9+
     build-depends: fail == 4.9.*,
@@ -171,5 +194,4 @@
 
 source-repository head
   type:     git
-  location: https://github.com/bos/attoparsec
-
+  location: https://github.com/bgamari/attoparsec
diff --git a/benchmarks/Aeson.hs b/benchmarks/Aeson.hs
--- a/benchmarks/Aeson.hs
+++ b/benchmarks/Aeson.hs
@@ -39,7 +39,7 @@
 import qualified Data.ByteString.Lazy as L
 import qualified Data.ByteString.Unsafe as B
 import qualified Data.HashMap.Strict as H
-import Criterion.Main
+import Test.Tasty.Bench
 
 #define BACKSLASH 92
 #define CLOSE_CURLY 125
diff --git a/benchmarks/Alternative.hs b/benchmarks/Alternative.hs
deleted file mode 100644
--- a/benchmarks/Alternative.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- This benchmark reveals a huge performance regression that showed up
--- under GHC 7.8.1 (https://github.com/bos/attoparsec/issues/56).
---
--- With GHC 7.6.3 and older, this program runs in 0.04 seconds.  Under
--- GHC 7.8.1 with (<|>) inlined, time jumps to 12 seconds!
-
-import Control.Applicative
-import Data.Text (Text)
-import qualified Data.Attoparsec.Text as A
-import qualified Data.Text as T
-
-testParser :: Text -> Either String Int
-testParser f = fmap length -- avoid printing out the entire matched list
-        . A.parseOnly (many ((() <$ A.string "b") <|> (() <$ A.anyChar)))
-        $ f
-
-main :: IO ()
-main = print . testParser $ T.replicate 50000 "a"
diff --git a/benchmarks/Benchmarks.hs b/benchmarks/Benchmarks.hs
--- a/benchmarks/Benchmarks.hs
+++ b/benchmarks/Benchmarks.hs
@@ -2,7 +2,7 @@
 
 import Common ()
 import Control.Applicative (many)
-import Criterion.Main (bench, bgroup, defaultMain, nf)
+import Test.Tasty.Bench (bench, bgroup, defaultMain, nf)
 import Data.Bits
 import Data.Char (isAlpha)
 import Data.Word (Word32)
diff --git a/benchmarks/Genome.hs b/benchmarks/Genome.hs
--- a/benchmarks/Genome.hs
+++ b/benchmarks/Genome.hs
@@ -6,7 +6,7 @@
     ) where
 
 import Control.Applicative
-import Criterion.Main
+import Test.Tasty.Bench
 import Data.ByteString (ByteString)
 import qualified Data.ByteString.Char8 as B8
 import qualified Data.ByteString.Lazy.Char8 as L8
diff --git a/benchmarks/HeadersByteString.hs b/benchmarks/HeadersByteString.hs
--- a/benchmarks/HeadersByteString.hs
+++ b/benchmarks/HeadersByteString.hs
@@ -1,14 +1,22 @@
 module HeadersByteString (headers) where
 
 import Common (pathTo, rechunkBS)
-import Criterion.Main (bench, bgroup, nf, nfIO)
-import Criterion.Types (Benchmark)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf, nfIO)
 import HeadersByteString.Atto (request, response)
 import Network.Wai.Handler.Warp.RequestHeader (parseHeaderLines)
 import qualified Data.Attoparsec.ByteString.Char8 as B
 import qualified Data.Attoparsec.ByteString.Lazy as BL
 import qualified Data.ByteString.Char8 as B
 
+-- Note: In the benchmarks for parsing an http request
+-- from a strict bytestring, we consider warp's implementation,
+-- which has highly optimized code for handling the first
+-- line in particular. It's treatment of the headers
+-- is more relaxed from the treatment they are given by the
+-- attoparsec parser benchmarked here. Consequently, it
+-- is should not be possible to match its performance since
+-- it accepts header names with disallowed characters.
+
 headers :: IO Benchmark
 headers = do
   req <- B.readFile =<< pathTo "http-request.txt"
@@ -18,7 +26,7 @@
   return $ bgroup "headers" [
       bgroup "B" [
         bench "request" $ nf (B.parseOnly request) req
-      , bench "warp" $ nfIO (parseHeaderLines [req])
+      , bench "warp" $ nfIO (parseHeaderLines (B.split '\n' req))
       , bench "response" $ nf (B.parseOnly response) resp
       ]
     , bgroup "BL" [
diff --git a/benchmarks/HeadersByteString/Atto.hs b/benchmarks/HeadersByteString/Atto.hs
--- a/benchmarks/HeadersByteString/Atto.hs
+++ b/benchmarks/HeadersByteString/Atto.hs
@@ -15,8 +15,16 @@
 instance NFData HttpVersion where
     rnf !_ = ()
 
+isHeaderChar :: Char -> Bool
+isHeaderChar c =
+  (c >= 'a' && c <= 'z') ||
+  (c >= 'A' && c <= 'Z') ||
+  (c >= '0' && c <= '9') ||
+  (c == '_') ||
+  (c == '-')
+
 header = do
-  name <- B.takeWhile1 (B.inClass "a-zA-Z0-9_-") <* B.char ':' <* B.skipSpace
+  name <- B.takeWhile1 isHeaderChar <* B.char ':' <* B.skipSpace
   body <- bodyLine
   return (name, body)
 
diff --git a/benchmarks/HeadersText.hs b/benchmarks/HeadersText.hs
--- a/benchmarks/HeadersText.hs
+++ b/benchmarks/HeadersText.hs
@@ -4,8 +4,7 @@
 
 import Common (pathTo, rechunkT)
 import Control.Applicative
-import Criterion.Main (bench, bgroup, nf)
-import Criterion.Types (Benchmark)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf)
 import Data.Char (isSpace)
 import qualified Data.Attoparsec.Text as T
 import qualified Data.Attoparsec.Text.Lazy as TL
diff --git a/benchmarks/IsSpace.hs b/benchmarks/IsSpace.hs
deleted file mode 100644
--- a/benchmarks/IsSpace.hs
+++ /dev/null
@@ -1,123 +0,0 @@
-{-# OPTIONS_GHC -Wall -fwarn-tabs #-}
-{-# LANGUAGE ForeignFunctionInterface #-}
-----------------------------------------------------------------
---                                                    2010.10.09
--- |
--- Module      :  IsSpace
--- Copyright   :  Copyright (c) 2010 wren ng thornton
--- License     :  BSD
--- Maintainer  :  wren@community.haskell.org
--- Stability   :  experimental
--- Portability :  portable (FFI)
---
--- A benchmark for comparing different definitions of predicates
--- for detecting whitespace. As of the last run the results are:
--- 
--- * Data.Char.isSpace             : 14.44786 us +/- 258.0377 ns
--- * isSpace_DataChar              : 43.25154 us +/- 655.7037 ns
--- * isSpace_Char                  : 29.26598 us +/- 454.1445 ns
--- * isPerlSpace                   :
--- * Data.Attoparsec.Char8.isSpace : 81.87335 us +/- 1.195903 us
--- * isSpace_Char8                 : 11.84677 us +/- 178.9795 ns
--- * isSpace_w8                    : 11.55470 us +/- 133.7644 ns
-----------------------------------------------------------------
-module IsSpace (main) where
-
-import qualified Data.Char             as C
-import           Data.Word             (Word8)
-import qualified Data.ByteString       as B
-import qualified Data.ByteString.Char8 as B8
-import           Foreign.C.Types       (CInt)
-
-import           Criterion             (bench, nf)
-import           Criterion.Main        (defaultMain)
-
-----------------------------------------------------------------
------ Character predicates
--- N.B. \x9..\xD == "\t\n\v\f\r"
-
--- | Recognize the same characters as Perl's @/\s/@ in Unicode mode.
--- In particular, we recognize POSIX 1003.2 @[[:space:]]@ except
--- @\'\v\'@, and recognize the Unicode @\'\x85\'@, @\'\x2028\'@,
--- @\'\x2029\'@. Notably, @\'\x85\'@ belongs to Latin-1 (but not
--- ASCII) and therefore does not belong to POSIX 1003.2 @[[:space:]]@
--- (nor non-Unicode @/\s/@).
-isPerlSpace :: Char -> Bool
-isPerlSpace c
-    =  (' '      == c)
-    || ('\t' <= c && c <= '\r' && c /= '\v')
-    || ('\x85'   == c)
-    || ('\x2028' == c)
-    || ('\x2029' == c)
-{-# INLINE isPerlSpace #-}
-
-
--- | 'Data.Attoparsec.Char8.isSpace', duplicated here because it's
--- not exported. This is the definition as of attoparsec-0.8.1.0.
-isSpace :: Char -> Bool
-isSpace c = c `B8.elem` spaces
-    where
-    spaces = B8.pack " \n\r\t\v\f"
-    {-# NOINLINE spaces #-}
-{-# INLINE isSpace #-}
-
-
--- | An alternate version of 'Data.Attoparsec.Char8.isSpace'.
-isSpace_Char8 :: Char -> Bool
-isSpace_Char8 c =  (' ' == c) || ('\t' <= c && c <= '\r')
-{-# INLINE isSpace_Char8 #-}
-
-
--- | An alternate version of 'Data.Char.isSpace'. This uses the
--- same trick as 'isSpace_Char8' but we include Unicode whitespaces
--- too, in order to have the same results as 'Data.Char.isSpace'
--- (whereas 'isSpace_Char8' doesn't recognize Unicode whitespace).
-isSpace_Char :: Char -> Bool
-isSpace_Char c
-    =  (' '    == c)
-    || ('\t' <= c && c <= '\r')
-    || ('\xA0' == c)
-    || (iswspace (fromIntegral (C.ord c)) /= 0)
-{-# INLINE isSpace_Char #-}
-
-foreign import ccall unsafe "u_iswspace"
-    iswspace :: CInt -> CInt
-
--- | Verbatim version of 'Data.Char.isSpace' (i.e., 'GHC.Unicode.isSpace'
--- as of base-4.2.0.2) in order to try to figure out why 'isSpace_Char'
--- is slower than 'Data.Char.isSpace'. It appears to be something
--- special in how the base library was compiled.
-isSpace_DataChar :: Char -> Bool
-isSpace_DataChar c =
-    c == ' '     ||
-    c == '\t'    ||
-    c == '\n'    ||
-    c == '\r'    ||
-    c == '\f'    ||
-    c == '\v'    ||
-    c == '\xa0'  ||
-    iswspace (fromIntegral (C.ord c)) /= 0
-{-# INLINE isSpace_DataChar #-}
-
-
--- | A 'Word8' version of 'Data.Attoparsec.Char8.isSpace'.
-isSpace_w8 :: Word8 -> Bool
-isSpace_w8 w = (w == 32) || (9 <= w && w <= 13)
-{-# INLINE isSpace_w8 #-}
-
-----------------------------------------------------------------
-
-main :: IO ()
-main = defaultMain
-    [ bench "Data.Char.isSpace" $ nf (map C.isSpace)        ['\x0'..'\255']
-    , bench "isSpace_DataChar"  $ nf (map isSpace_DataChar) ['\x0'..'\255']
-    , bench "isSpace_Char"      $ nf (map isSpace_Char)     ['\x0'..'\255']
-    , bench "isPerlSpace"       $ nf (map isPerlSpace)      ['\x0'..'\255']
-    , bench "Data.Attoparsec.Char8.isSpace"
-                                $ nf (map isSpace)          ['\x0'..'\255']
-    , bench "isSpace_Char8"     $ nf (map isSpace_Char8)    ['\x0'..'\255']
-    , bench "isSpace_w8"        $ nf (map isSpace_w8)       [0..255]
-    ]
-
-----------------------------------------------------------------
------------------------------------------------------------ fin.
diff --git a/benchmarks/Links.hs b/benchmarks/Links.hs
--- a/benchmarks/Links.hs
+++ b/benchmarks/Links.hs
@@ -4,7 +4,7 @@
 
 import Control.Applicative
 import Control.DeepSeq (NFData(..))
-import Criterion.Main (Benchmark, bench, nf)
+import Test.Tasty.Bench (Benchmark, bench, nf)
 import Data.Attoparsec.ByteString as A
 import Data.Attoparsec.ByteString.Char8 as A8
 import Data.ByteString.Char8 as B8
diff --git a/benchmarks/Main.hs b/benchmarks/Main.hs
deleted file mode 100644
--- a/benchmarks/Main.hs
+++ /dev/null
@@ -1,4 +0,0 @@
-import Sets
-import Criterion.Main
-
-main = defaultMain [Sets.benchmarks]
diff --git a/benchmarks/Numbers.hs b/benchmarks/Numbers.hs
--- a/benchmarks/Numbers.hs
+++ b/benchmarks/Numbers.hs
@@ -3,8 +3,7 @@
 
 module Numbers (numbers) where
 
-import Criterion.Main (bench, bgroup, nf)
-import Criterion.Types (Benchmark)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf)
 import Data.Scientific (Scientific(..))
 import Text.Parsec.Text ()
 import Text.Parsec.Text.Lazy ()
diff --git a/benchmarks/Sets.hs b/benchmarks/Sets.hs
--- a/benchmarks/Sets.hs
+++ b/benchmarks/Sets.hs
@@ -1,6 +1,6 @@
 module Sets (benchmarks) where
 
-import Criterion
+import Test.Tasty.Bench
 import Data.Char (ord)
 import qualified Data.Attoparsec.Text.FastSet as FastSet
 import qualified TextFastSet
diff --git a/benchmarks/Tiny.hs b/benchmarks/Tiny.hs
deleted file mode 100644
--- a/benchmarks/Tiny.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-import Control.Applicative ((<|>), many)
-import Control.Monad (forM_)
-import System.Environment (getArgs)
-import qualified Data.Attoparsec.ByteString.Char8 as A
-import qualified Data.ByteString.Char8 as B
-import qualified Text.Parsec as P
-import qualified Text.Parsec.ByteString as P
-
-attoparsec = do
-  args <- getArgs
-  forM_ args $ \arg -> do
-    input <- B.readFile arg
-    case A.parse p input `A.feed` B.empty of
-      A.Done _ xs -> print (length xs)
-      what        -> print what
- where
-  slow = many (A.many1 A.letter_ascii <|> A.many1 A.digit)
-  fast = many (A.takeWhile1 isLetter <|> A.takeWhile1 isDigit)
-  isDigit c  = c >= '0' && c <= '9'
-  isLetter c = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
-  p = fast
-
-parsec = do
-  args <- getArgs
-  forM_ args $ \arg -> do
-    input <- readFile arg
-    case P.parse (P.many (P.many1 P.letter P.<|> P.many1 P.digit)) "" input of
-      Left err -> print err
-      Right xs -> print (length xs)
-
-main = attoparsec
diff --git a/benchmarks/Warp.hs b/benchmarks/Warp.hs
--- a/benchmarks/Warp.hs
+++ b/benchmarks/Warp.hs
@@ -2,8 +2,7 @@
 
 module Warp (benchmarks) where
 
-import Criterion.Main (bench, bgroup, nf)
-import Criterion.Types (Benchmark)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf)
 import Data.ByteString (ByteString)
 import Network.Wai.Handler.Warp.ReadInt (readInt)
 import qualified Data.Attoparsec.ByteString.Char8 as B
diff --git a/benchmarks/attoparsec-benchmarks.cabal b/benchmarks/attoparsec-benchmarks.cabal
deleted file mode 100644
--- a/benchmarks/attoparsec-benchmarks.cabal
+++ /dev/null
@@ -1,41 +0,0 @@
--- These benchmarks are not intended to be installed.
--- So don't install 'em.
-
-name: attoparsec-benchmarks
-version: 0
-cabal-version: >=1.6
-build-type: Simple
-
-executable attoparsec-benchmarks
-  main-is: Benchmarks.hs
-  other-modules:
-    Common
-    HeadersByteString
-    HeadersByteString.Atto
-    HeadersText
-    Links
-    Numbers
-    Network.Wai.Handler.Warp.ReadInt
-    Sets
-    TextFastSet
-    Warp
-  hs-source-dirs: .. . warp-3.0.1.1
-  ghc-options: -O2 -Wall -rtsopts
-  build-depends:
-    array,
-    base == 4.*,
-    bytestring >= 0.10.4.0,
-    case-insensitive,
-    containers,
-    criterion >= 1.0,
-    deepseq >= 1.1,
-    directory,
-    filepath,
-    ghc-prim,
-    http-types,
-    parsec >= 3.1.2,
-    scientific >= 0.3.1,
-    text >= 1.1.1.0,
-    transformers,
-    unordered-containers,
-    vector
diff --git a/benchmarks/http-request.txt b/benchmarks/http-request.txt
--- a/benchmarks/http-request.txt
+++ b/benchmarks/http-request.txt
@@ -1,14 +1,9 @@
 GET / HTTP/1.1
 Host: twitter.com
-Accept: text/html, application/xhtml+xml, application/xml; q=0.9,
-	image/webp, */*; q=0.8
+Accept: text/html, application/xhtml+xml, application/xml; q=0.9, image/webp, */*; q=0.8
 Accept-Encoding: gzip,deflate,sdch
 Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
 Cache-Control: max-age=0
-Cookie: guest_id=v1%3A139; _twitter_sess=BAh7CSIKZmxhc2hJQz-e1e1;
-	__utma=43838368.452555194.1399611824.1; __utmb=43838368;
-	__utmc=43838368; __utmz=1399611824.1.1.utmcsr=(direct)|utmcmd=(none)
-User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5)
-	AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.86
-	Safari/537.36
+Cookie: guest_id=v1%3A139; _twitter_sess=BAh7CSIKZmxhc2hJQz-e1e1; __utma=43838368.452555194.1399611824.1; __utmb=43838368; __utmc=43838368; __utmz=1399611824.1.1.utmcsr=(direct)|utmcmd=(none)
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.86 Safari/537.36
 
diff --git a/benchmarks/http-response.txt b/benchmarks/http-response.txt
--- a/benchmarks/http-response.txt
+++ b/benchmarks/http-response.txt
@@ -3,10 +3,8 @@
 Expires: -1
 Cache-Control: private, max-age=0
 Content-Type: text/html; charset=ISO-8859-1
-Set-Cookie: PREF=ID=1a871afc4240db5e:FF:LM=1399609497:S=e5MZ0itEekjVwNcU;
-	expires=Sun, 08-May-2016 04:24:57 GMT; path=/; domain=.google.com
-Set-Cookie: NID=67=-WlfaDG6VSEPk7abAjrK98HBSoCD2ID6JKkUR95tEumzDmg7Fc8pSQ8;
-	expires=Sat, 08-Nov-2014 04:24:57 GMT; path=/; domain=.google.com; HttpOnly
+Set-Cookie: PREF=ID=1a871afc4240db5e:FF:LM=1399609497:S=e5MZ0itEekjVwNcU; expires=Sun, 08-May-2016 04:24:57 GMT; path=/; domain=.google.com
+Set-Cookie: NID=67=-WlfaDG6VSEPk7abAjrK98HBSoCD2ID6JKkUR95tEumzDmg7Fc8pSQ8; expires=Sat, 08-Nov-2014 04:24:57 GMT; path=/; domain=.google.com; HttpOnly
 P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
 Server: gws
 X-XSS-Protection: 1; mode=block
diff --git a/benchmarks/warp-3.0.1.1/Network/Wai/Handler/Warp/ReadInt.hs b/benchmarks/warp-3.0.1.1/Network/Wai/Handler/Warp/ReadInt.hs
--- a/benchmarks/warp-3.0.1.1/Network/Wai/Handler/Warp/ReadInt.hs
+++ b/benchmarks/warp-3.0.1.1/Network/Wai/Handler/Warp/ReadInt.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE MagicHash #-}
 {-# LANGUAGE BangPatterns #-}
@@ -41,7 +42,11 @@
 
 {-# NOINLINE mhDigitToInt #-}
 mhDigitToInt :: Word8 -> Int
+#if MIN_VERSION_base(4,16,0)
+mhDigitToInt (W8# i) = I# (word2Int# (word8ToWord# (indexWord8OffAddr# addr (word2Int# (word8ToWord# i)))))
+#else
 mhDigitToInt (W8# i) = I# (word2Int# (indexWord8OffAddr# addr (word2Int# i)))
+#endif
   where
     !(Table addr) = table
     table :: Table
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,36 +1,67 @@
-0.13.1.0
+# 0.14.4
 
+* Fix a segmentation fault when built against `text-2.0`
+* Restructure project to allow more convenient usage of benchmark suite
+* Allow benchmarks to build with GHC 9.2
+
+# 0.14.3
+
+* Support for GHC 9.2.1
+
+# 0.14.2
+
+* Support for GHC 9.2.1
+
+# 0.14.1
+
+* Added `Data.Attoparsec.ByteString.getChunk`.
+
+# 0.14.0
+
+* Added `Data.Attoparsec.ByteString.takeWhileIncluding`.
+* Make `Data.Attoparsec.{Text,ByteString}.Lazy.parseOnly` accept lazy input.
+
+# 0.13.2.1
+
+* Improved performance of `Data.Attoparsec.Text.asciiCI`
+
+# 0.13.2.0
+
+* `pure` is now strict in `Position`
+
+# 0.13.1.0
+
 * `runScanner` now correctly returns the final state
   (https://github.com/bos/attoparsec/issues/105).
 * `Parser`, `ZeptoT`, `Buffer`, and `More` now expose `Semigroup` instances.
 * `Parser`, and `ZeptoT` now expose `MonadFail` instances.
 
-0.13.0.2
+# 0.13.0.2
 
 * Restore the fast specialised character set implementation for Text
 * Move testsuite from test-framework to tasty
 * Performance optimization of takeWhile and takeWhile1
 
-0.13.0.1
+# 0.13.0.1
 
 * Fixed a bug in the implementations of inClass and notInClass for
   Text (https://github.com/bos/attoparsec/issues/103)
 
-0.13.0.0
+# 0.13.0.0
 
 * Made the parser type in the Zepto module a monad transformer
   (needed by aeson's string unescaping parser).
 
-0.12.1.6
+# 0.12.1.6
 
 * Fixed a case folding bug in the ByteString version of stringCI.
 
-0.12.1.5
+# 0.12.1.5
 
 * Fixed an indexing bug in the new Text implementation of string,
   reported by Michel Boucey.
 
-0.12.1.4
+# 0.12.1.4
 
 * Fixed a case where the string parser would consume an unnecessary
   amount of input before failing a match, when it could bail much
@@ -39,29 +70,29 @@
 * Added more context to error messages
   (https://github.com/bos/attoparsec/pull/79)
 
-0.12.1.3
+# 0.12.1.3
 
 * Fixed incorrect tracking of Text lengths
   (https://github.com/bos/attoparsec/issues/80)
 
-0.12.1.2
+# 0.12.1.2
 
 * Fixed the incorrect tracking of capacity if the initial buffer was
   empty (https://github.com/bos/attoparsec/issues/75)
 
-0.12.1.1
+# 0.12.1.1
 
 * Fixed a data corruption bug that occurred under some circumstances
   if a buffer grew after prompting for more input
   (https://github.com/bos/attoparsec/issues/74)
 
-0.12.1.0
+# 0.12.1.0
 
 * Now compatible with GHC 7.9
 
 * Reintroduced the Chunk class, used by the parsers package
 
-0.12.0.0
+# 0.12.0.0
 
 * A new internal representation makes almost all real-world parsers
   faster, sometimes by big margins.  For example, parsing JSON data
@@ -83,12 +114,12 @@
 * A few obsolete modules and functions have been marked as deprecated.
   They will be removed from the next major release.
 
-0.11.3.0
+# 0.11.3.0
 
 * New function scientific is compatible with rational, but parses
   integers more efficiently (https://github.com/bos/aeson/issues/198)
 
-0.11.2.0
+# 0.11.2.0
 
 * The new Chunk typeclass allows for some code sharing with Ed
   Kmett's parsers package: http://hackage.haskell.org/package/parsers
@@ -96,8 +127,7 @@
 * New function runScanner generalises scan to return the final state
   of the scanner as well as the input consumed.
 
-
-0.11.1.0
+# 0.11.1.0
 
 * New dependency: the scientific package.  This allows us to parse
   numbers much more efficiently.
diff --git a/internal/Data/Attoparsec/ByteString/Buffer.hs b/internal/Data/Attoparsec/ByteString/Buffer.hs
new file mode 100644
--- /dev/null
+++ b/internal/Data/Attoparsec/ByteString/Buffer.hs
@@ -0,0 +1,157 @@
+{-# LANGUAGE BangPatterns #-}
+-- |
+-- Module      :  Data.Attoparsec.ByteString.Buffer
+-- Copyright   :  Bryan O'Sullivan 2007-2015
+-- License     :  BSD3
+--
+-- Maintainer  :  bos@serpentine.com
+-- Stability   :  experimental
+-- Portability :  GHC
+--
+-- An "immutable" buffer that supports cheap appends.
+--
+-- A Buffer is divided into an immutable read-only zone, followed by a
+-- mutable area that we've preallocated, but not yet written to.
+--
+-- We overallocate at the end of a Buffer so that we can cheaply
+-- append.  Since a user of an existing Buffer cannot see past the end
+-- of its immutable zone into the data that will change during an
+-- append, this is safe.
+--
+-- Once we run out of space at the end of a Buffer, we do the usual
+-- doubling of the buffer size.
+--
+-- The fact of having a mutable buffer really helps with performance,
+-- but it does have a consequence: if someone misuses the Partial API
+-- that attoparsec uses by calling the same continuation repeatedly
+-- (which never makes sense in practice), they could overwrite data.
+--
+-- Since the API *looks* pure, it should *act* pure, too, so we use
+-- two generation counters (one mutable, one immutable) to track the
+-- number of appends to a mutable buffer. If the counters ever get out
+-- of sync, someone is appending twice to a mutable buffer, so we
+-- duplicate the entire buffer in order to preserve the immutability
+-- of its older self.
+--
+-- While we could go a step further and gain protection against API
+-- abuse on a multicore system, by use of an atomic increment
+-- instruction to bump the mutable generation counter, that would be
+-- very expensive, and feels like it would also be in the realm of the
+-- ridiculous.  Clients should never call a continuation more than
+-- once; we lack a linear type system that could enforce this; and
+-- there's only so far we should go to accommodate broken uses.
+
+module Data.Attoparsec.ByteString.Buffer
+    (
+      Buffer
+    , buffer
+    , unbuffer
+    , pappend
+    , length
+    , unsafeIndex
+    , substring
+    , unsafeDrop
+    ) where
+
+import Control.Exception (assert)
+import Data.ByteString.Internal (ByteString(..), memcpy, nullForeignPtr)
+import Data.Attoparsec.Internal.Fhthagn (inlinePerformIO)
+import Data.Attoparsec.Internal.Compat
+import Data.List (foldl1')
+import Data.Monoid as Mon (Monoid(..))
+import Data.Semigroup (Semigroup(..))
+import Data.Word (Word8)
+import Foreign.ForeignPtr (ForeignPtr, withForeignPtr)
+import Foreign.Ptr (castPtr, plusPtr)
+import Foreign.Storable (peek, peekByteOff, poke, sizeOf)
+import GHC.ForeignPtr (mallocPlainForeignPtrBytes)
+import Prelude hiding (length)
+
+-- If _cap is zero, this buffer is empty.
+data Buffer = Buf {
+      _fp  :: {-# UNPACK #-} !(ForeignPtr Word8)
+    , _off :: {-# UNPACK #-} !Int
+    , _len :: {-# UNPACK #-} !Int
+    , _cap :: {-# UNPACK #-} !Int
+    , _gen :: {-# UNPACK #-} !Int
+    }
+
+instance Show Buffer where
+    showsPrec p = showsPrec p . unbuffer
+
+-- | The initial 'Buffer' has no mutable zone, so we can avoid all
+-- copies in the (hopefully) common case of no further input being fed
+-- to us.
+buffer :: ByteString -> Buffer
+buffer bs = withPS bs $ \fp off len -> Buf fp off len len 0
+
+unbuffer :: Buffer -> ByteString
+unbuffer (Buf fp off len _ _) = mkPS fp off len
+
+instance Semigroup Buffer where
+    (Buf _ _ _ 0 _) <> b                    = b
+    a               <> (Buf _ _ _ 0 _)      = a
+    buf             <> (Buf fp off len _ _) = append buf fp off len
+
+instance Monoid Buffer where
+    mempty = Buf nullForeignPtr 0 0 0 0
+
+    mappend = (<>)
+
+    mconcat [] = Mon.mempty
+    mconcat xs = foldl1' mappend xs
+
+pappend :: Buffer -> ByteString -> Buffer
+pappend (Buf _ _ _ 0 _) bs  = buffer bs
+pappend buf             bs  = withPS bs $ \fp off len -> append buf fp off len
+
+append :: Buffer -> ForeignPtr a -> Int -> Int -> Buffer
+append (Buf fp0 off0 len0 cap0 gen0) !fp1 !off1 !len1 =
+  inlinePerformIO . withForeignPtr fp0 $ \ptr0 ->
+    withForeignPtr fp1 $ \ptr1 -> do
+      let genSize = sizeOf (0::Int)
+          newlen  = len0 + len1
+      gen <- if gen0 == 0
+             then return 0
+             else peek (castPtr ptr0)
+      if gen == gen0 && newlen <= cap0
+        then do
+          let newgen = gen + 1
+          poke (castPtr ptr0) newgen
+          memcpy (ptr0 `plusPtr` (off0+len0))
+                 (ptr1 `plusPtr` off1)
+                 (fromIntegral len1)
+          return (Buf fp0 off0 newlen cap0 newgen)
+        else do
+          let newcap = newlen * 2
+          fp <- mallocPlainForeignPtrBytes (newcap + genSize)
+          withForeignPtr fp $ \ptr_ -> do
+            let ptr    = ptr_ `plusPtr` genSize
+                newgen = 1
+            poke (castPtr ptr_) newgen
+            memcpy ptr (ptr0 `plusPtr` off0) (fromIntegral len0)
+            memcpy (ptr `plusPtr` len0) (ptr1 `plusPtr` off1)
+                   (fromIntegral len1)
+            return (Buf fp genSize newlen newcap newgen)
+
+length :: Buffer -> Int
+length (Buf _ _ len _ _) = len
+{-# INLINE length #-}
+
+unsafeIndex :: Buffer -> Int -> Word8
+unsafeIndex (Buf fp off len _ _) i = assert (i >= 0 && i < len) .
+    inlinePerformIO . withForeignPtr fp $ flip peekByteOff (off+i)
+{-# INLINE unsafeIndex #-}
+
+substring :: Int -> Int -> Buffer -> ByteString
+substring s l (Buf fp off len _ _) =
+  assert (s >= 0 && s <= len) .
+  assert (l >= 0 && l <= len-s) $
+  mkPS fp (off+s) l
+{-# INLINE substring #-}
+
+unsafeDrop :: Int -> Buffer -> ByteString
+unsafeDrop s (Buf fp off len _ _) =
+  assert (s >= 0 && s <= len) $
+  mkPS fp (off+s) (len-s)
+{-# INLINE unsafeDrop #-}
diff --git a/internal/Data/Attoparsec/ByteString/FastSet.hs b/internal/Data/Attoparsec/ByteString/FastSet.hs
new file mode 100644
--- /dev/null
+++ b/internal/Data/Attoparsec/ByteString/FastSet.hs
@@ -0,0 +1,112 @@
+{-# LANGUAGE BangPatterns, MagicHash #-}
+
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Attoparsec.ByteString.FastSet
+-- Copyright   :  Bryan O'Sullivan 2007-2015
+-- License     :  BSD3
+--
+-- Maintainer  :  bos@serpentine.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Fast set membership tests for 'Word8' and 8-bit 'Char' values.  The
+-- set representation is unboxed for efficiency.  For small sets, we
+-- test for membership using a binary search.  For larger sets, we use
+-- a lookup table.
+--
+-----------------------------------------------------------------------------
+module Data.Attoparsec.ByteString.FastSet
+    (
+    -- * Data type
+      FastSet
+    -- * Construction
+    , fromList
+    , set
+    -- * Lookup
+    , memberChar
+    , memberWord8
+    -- * Debugging
+    , fromSet
+    -- * Handy interface
+    , charClass
+    ) where
+
+import Data.Bits ((.&.), (.|.), unsafeShiftL)
+import Foreign.Storable (peekByteOff, pokeByteOff)
+import GHC.Exts (Int(I#), iShiftRA#)
+import GHC.Word (Word8)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.ByteString.Internal as I
+import qualified Data.ByteString.Unsafe as U
+
+data FastSet = Sorted { fromSet :: !B.ByteString }
+             | Table  { fromSet :: !B.ByteString }
+    deriving (Eq, Ord)
+
+instance Show FastSet where
+    show (Sorted s) = "FastSet Sorted " ++ show (B8.unpack s)
+    show (Table _) = "FastSet Table"
+
+-- | The lower bound on the size of a lookup table.  We choose this to
+-- balance table density against performance.
+tableCutoff :: Int
+tableCutoff = 8
+
+-- | Create a set.
+set :: B.ByteString -> FastSet
+set s | B.length s < tableCutoff = Sorted . B.sort $ s
+      | otherwise                = Table . mkTable $ s
+
+fromList :: [Word8] -> FastSet
+fromList = set . B.pack
+
+data I = I {-# UNPACK #-} !Int {-# UNPACK #-} !Word8
+
+shiftR :: Int -> Int -> Int
+shiftR (I# x#) (I# i#) = I# (x# `iShiftRA#` i#)
+
+index :: Int -> I
+index i = I (i `shiftR` 3) (1 `unsafeShiftL` (i .&. 7))
+{-# INLINE index #-}
+
+-- | Check the set for membership.
+memberWord8 :: Word8 -> FastSet -> Bool
+memberWord8 w (Table t)  =
+    let I byte bit = index (fromIntegral w)
+    in  U.unsafeIndex t byte .&. bit /= 0
+memberWord8 w (Sorted s) = search 0 (B.length s - 1)
+    where search lo hi
+              | hi < lo = False
+              | otherwise =
+                  let mid = (lo + hi) `quot` 2
+                  in case compare w (U.unsafeIndex s mid) of
+                       GT -> search (mid + 1) hi
+                       LT -> search lo (mid - 1)
+                       _ -> True
+
+-- | Check the set for membership.  Only works with 8-bit characters:
+-- characters above code point 255 will give wrong answers.
+memberChar :: Char -> FastSet -> Bool
+memberChar c = memberWord8 (I.c2w c)
+{-# INLINE memberChar #-}
+
+mkTable :: B.ByteString -> B.ByteString
+mkTable s = I.unsafeCreate 32 $ \t -> do
+            _ <- I.memset t 0 32
+            U.unsafeUseAsCStringLen s $ \(p, l) ->
+              let loop n | n == l = return ()
+                         | otherwise = do
+                    c <- peekByteOff p n :: IO Word8
+                    let I byte bit = index (fromIntegral c)
+                    prev <- peekByteOff t byte :: IO Word8
+                    pokeByteOff t byte (prev .|. bit)
+                    loop (n + 1)
+              in loop 0
+
+charClass :: String -> FastSet
+charClass = set . B8.pack . go
+    where go (a:'-':b:xs) = [a..b] ++ go xs
+          go (x:xs) = x : go xs
+          go _ = ""
diff --git a/internal/Data/Attoparsec/Internal/Compat.hs b/internal/Data/Attoparsec/Internal/Compat.hs
new file mode 100644
--- /dev/null
+++ b/internal/Data/Attoparsec/Internal/Compat.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE MagicHash #-}
+module Data.Attoparsec.Internal.Compat where
+
+import Data.ByteString.Internal (ByteString(..))
+import Data.Word (Word8)
+import Foreign.ForeignPtr (ForeignPtr)
+
+#if MIN_VERSION_bytestring(0,11,0)
+import Data.ByteString.Internal (plusForeignPtr)
+#endif
+
+withPS :: ByteString -> (ForeignPtr Word8 -> Int -> Int -> r) -> r
+#if MIN_VERSION_bytestring(0,11,0)
+withPS (BS fp len)     kont = kont fp 0   len
+#else
+withPS (PS fp off len) kont = kont fp off len
+#endif
+{-# INLINE withPS #-}
+
+mkPS :: ForeignPtr Word8 -> Int -> Int -> ByteString
+#if MIN_VERSION_bytestring(0,11,0)
+mkPS fp off len = BS (plusForeignPtr fp off) len
+#else
+mkPS fp off len = PS fp off len
+#endif
+{-# INLINE mkPS #-}
diff --git a/internal/Data/Attoparsec/Internal/Fhthagn.hs b/internal/Data/Attoparsec/Internal/Fhthagn.hs
new file mode 100644
--- /dev/null
+++ b/internal/Data/Attoparsec/Internal/Fhthagn.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE BangPatterns, Rank2Types, OverloadedStrings,
+    RecordWildCards, MagicHash, UnboxedTuples #-}
+
+module Data.Attoparsec.Internal.Fhthagn
+    (
+      inlinePerformIO
+    ) where
+
+import GHC.Exts (realWorld#)
+import GHC.IO (IO(IO))
+
+-- | Just like unsafePerformIO, but we inline it. Big performance gains as
+-- it exposes lots of things to further inlining. /Very unsafe/. In
+-- particular, you should do no memory allocation inside an
+-- 'inlinePerformIO' block. On Hugs this is just @unsafePerformIO@.
+inlinePerformIO :: IO a -> a
+inlinePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
+{-# INLINE inlinePerformIO #-}
diff --git a/internal/Data/Attoparsec/Text/Buffer.hs b/internal/Data/Attoparsec/Text/Buffer.hs
new file mode 100644
--- /dev/null
+++ b/internal/Data/Attoparsec/Text/Buffer.hs
@@ -0,0 +1,235 @@
+{-# LANGUAGE BangPatterns, CPP, MagicHash, RankNTypes, RecordWildCards,
+    UnboxedTuples #-}
+
+-- |
+-- Module      :  Data.Attoparsec.Text.Buffer
+-- Copyright   :  Bryan O'Sullivan 2007-2015
+-- License     :  BSD3
+--
+-- Maintainer  :  bos@serpentine.com
+-- Stability   :  experimental
+-- Portability :  GHC
+--
+-- An immutable buffer that supports cheap appends.
+
+-- A Buffer is divided into an immutable read-only zone, followed by a
+-- mutable area that we've preallocated, but not yet written to.
+--
+-- We overallocate at the end of a Buffer so that we can cheaply
+-- append.  Since a user of an existing Buffer cannot see past the end
+-- of its immutable zone into the data that will change during an
+-- append, this is safe.
+--
+-- Once we run out of space at the end of a Buffer, we do the usual
+-- doubling of the buffer size.
+
+module Data.Attoparsec.Text.Buffer
+    (
+      Buffer
+    , buffer
+    , unbuffer
+    , unbufferAt
+    , length
+    , pappend
+    , iter
+    , iter_
+    , substring
+    , lengthCodeUnits
+    , dropCodeUnits
+    ) where
+
+import Control.Exception (assert)
+import Data.List (foldl1')
+import Data.Monoid as Mon (Monoid(..))
+import Data.Semigroup (Semigroup(..))
+import Data.Text ()
+import Data.Text.Internal (Text(..))
+#if MIN_VERSION_text(2,0,0)
+import Data.Text.Internal.Encoding.Utf8 (utf8LengthByLeader)
+import Data.Text.Unsafe (iterArray, lengthWord8)
+#else
+import Data.Bits (shiftR)
+import Data.Text.Internal.Encoding.Utf16 (chr2)
+import Data.Text.Internal.Unsafe.Char (unsafeChr)
+import Data.Text.Unsafe (lengthWord16)
+#endif
+import Data.Text.Unsafe (Iter(..))
+import Foreign.Storable (sizeOf)
+import GHC.Exts (Int(..), indexIntArray#, unsafeCoerce#, writeIntArray#)
+import GHC.ST (ST(..), runST)
+import Prelude hiding (length)
+import qualified Data.Text.Array as A
+
+-- If _cap is zero, this buffer is empty.
+data Buffer = Buf {
+      _arr :: {-# UNPACK #-} !A.Array
+    , _off :: {-# UNPACK #-} !Int
+    , _len :: {-# UNPACK #-} !Int
+    , _cap :: {-# UNPACK #-} !Int
+    , _gen :: {-# UNPACK #-} !Int
+    }
+
+instance Show Buffer where
+    showsPrec p = showsPrec p . unbuffer
+
+-- | The initial 'Buffer' has no mutable zone, so we can avoid all
+-- copies in the (hopefully) common case of no further input being fed
+-- to us.
+buffer :: Text -> Buffer
+buffer (Text arr off len) = Buf arr off len len 0
+
+unbuffer :: Buffer -> Text
+unbuffer (Buf arr off len _ _) = Text arr off len
+
+unbufferAt :: Int -> Buffer -> Text
+unbufferAt s (Buf arr off len _ _) =
+  assert (s >= 0 && s <= len) $
+  Text arr (off+s) (len-s)
+
+instance Semigroup Buffer where
+    (Buf _ _ _ 0 _) <> b                     = b
+    a               <> (Buf _ _ _ 0 _)       = a
+    buf             <> (Buf arr off len _ _) = append buf arr off len
+    {-# INLINE (<>) #-}
+
+instance Monoid Buffer where
+    mempty = Buf A.empty 0 0 0 0
+    {-# INLINE mempty #-}
+
+    mappend = (<>)
+
+    mconcat [] = Mon.mempty
+    mconcat xs = foldl1' (<>) xs
+
+pappend :: Buffer -> Text -> Buffer
+pappend (Buf _ _ _ 0 _) t      = buffer t
+pappend buf (Text arr off len) = append buf arr off len
+
+append :: Buffer -> A.Array -> Int -> Int -> Buffer
+append (Buf arr0 off0 len0 cap0 gen0) !arr1 !off1 !len1 = runST $ do
+#if MIN_VERSION_text(2,0,0)
+  let woff    = sizeOf (0::Int)
+#else
+  let woff    = sizeOf (0::Int) `shiftR` 1
+#endif
+      newlen  = len0 + len1
+      !gen    = if gen0 == 0 then 0 else readGen arr0
+  if gen == gen0 && newlen <= cap0
+    then do
+      let newgen = gen + 1
+      marr <- unsafeThaw arr0
+      writeGen marr newgen
+#if MIN_VERSION_text(2,0,0)
+      A.copyI len1 marr (off0+len0) arr1 off1
+#else
+      A.copyI marr (off0+len0) arr1 off1 (off0+newlen)
+#endif
+      arr2 <- A.unsafeFreeze marr
+      return (Buf arr2 off0 newlen cap0 newgen)
+    else do
+      let newcap = newlen * 2
+          newgen = 1
+      marr <- A.new (newcap + woff)
+      writeGen marr newgen
+#if MIN_VERSION_text(2,0,0)
+      A.copyI len0 marr woff arr0 off0
+      A.copyI len1 marr (woff+len0) arr1 off1
+#else
+      A.copyI marr woff arr0 off0 (woff+len0)
+      A.copyI marr (woff+len0) arr1 off1 (woff+newlen)
+#endif
+      arr2 <- A.unsafeFreeze marr
+      return (Buf arr2 woff newlen newcap newgen)
+
+length :: Buffer -> Int
+length (Buf _ _ len _ _) = len
+{-# INLINE length #-}
+
+substring :: Int -> Int -> Buffer -> Text
+substring s l (Buf arr off len _ _) =
+  assert (s >= 0 && s <= len) .
+  assert (l >= 0 && l <= len-s) $
+  Text arr (off+s) l
+{-# INLINE substring #-}
+
+#if MIN_VERSION_text(2,0,0)
+
+lengthCodeUnits :: Text -> Int
+lengthCodeUnits = lengthWord8
+
+dropCodeUnits :: Int -> Buffer -> Text
+dropCodeUnits s (Buf arr off len _ _) =
+  assert (s >= 0 && s <= len) $
+  Text arr (off+s) (len-s)
+{-# INLINE dropCodeUnits #-}
+
+-- | /O(1)/ Iterate (unsafely) one step forwards through a UTF-8
+-- array, returning the current character and the delta to add to give
+-- the next offset to iterate at.
+iter :: Buffer -> Int -> Iter
+iter (Buf arr off _ _ _) i = iterArray arr (off + i)
+{-# INLINE iter #-}
+
+-- | /O(1)/ Iterate one step through a UTF-8 array, returning the
+-- delta to add to give the next offset to iterate at.
+iter_ :: Buffer -> Int -> Int
+iter_ (Buf arr off _ _ _) i = utf8LengthByLeader $ A.unsafeIndex arr (off+i)
+{-# INLINE iter_ #-}
+
+unsafeThaw :: A.Array -> ST s (A.MArray s)
+unsafeThaw (A.ByteArray a) = ST $ \s# ->
+                          (# s#, A.MutableByteArray (unsafeCoerce# a) #)
+
+readGen :: A.Array -> Int
+readGen (A.ByteArray a) = case indexIntArray# a 0# of r# -> I# r#
+
+writeGen :: A.MArray s -> Int -> ST s ()
+writeGen (A.MutableByteArray a) (I# gen#) = ST $ \s0# ->
+  case writeIntArray# a 0# gen# s0# of
+    s1# -> (# s1#, () #)
+
+#else
+
+lengthCodeUnits :: Text -> Int
+lengthCodeUnits = lengthWord16
+
+dropCodeUnits :: Int -> Buffer -> Text
+dropCodeUnits s (Buf arr off len _ _) =
+  assert (s >= 0 && s <= len) $
+  Text arr (off+s) (len-s)
+{-# INLINE dropCodeUnits #-}
+
+-- | /O(1)/ Iterate (unsafely) one step forwards through a UTF-16
+-- array, returning the current character and the delta to add to give
+-- the next offset to iterate at.
+iter :: Buffer -> Int -> Iter
+iter (Buf arr off _ _ _) i
+    | m < 0xD800 || m > 0xDBFF = Iter (unsafeChr m) 1
+    | otherwise                = Iter (chr2 m n) 2
+  where m = A.unsafeIndex arr j
+        n = A.unsafeIndex arr k
+        j = off + i
+        k = j + 1
+{-# INLINE iter #-}
+
+-- | /O(1)/ Iterate one step through a UTF-16 array, returning the
+-- delta to add to give the next offset to iterate at.
+iter_ :: Buffer -> Int -> Int
+iter_ (Buf arr off _ _ _) i | m < 0xD800 || m > 0xDBFF = 1
+                                | otherwise                = 2
+  where m = A.unsafeIndex arr (off+i)
+{-# INLINE iter_ #-}
+
+unsafeThaw :: A.Array -> ST s (A.MArray s)
+unsafeThaw A.Array{..} = ST $ \s# ->
+                          (# s#, A.MArray (unsafeCoerce# aBA) #)
+
+readGen :: A.Array -> Int
+readGen a = case indexIntArray# (A.aBA a) 0# of r# -> I# r#
+
+writeGen :: A.MArray s -> Int -> ST s ()
+writeGen a (I# gen#) = ST $ \s0# ->
+  case writeIntArray# (A.maBA a) 0# gen# s0# of
+    s1# -> (# s1#, () #)
+
+#endif
diff --git a/internal/Data/Attoparsec/Text/FastSet.hs b/internal/Data/Attoparsec/Text/FastSet.hs
new file mode 100644
--- /dev/null
+++ b/internal/Data/Attoparsec/Text/FastSet.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE BangPatterns #-}
+
+------------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Attoparsec.FastSet
+-- Copyright   :  Felipe Lessa 2010, Bryan O'Sullivan 2007-2015
+-- License     :  BSD3
+--
+-- Maintainer  :  felipe.lessa@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Fast set membership tests for 'Char' values. We test for
+-- membership using a hashtable implemented with Robin Hood
+-- collision resolution. The set representation is unboxed,
+-- and the characters and hashes interleaved, for efficiency.
+--
+--
+-----------------------------------------------------------------------------
+module Data.Attoparsec.Text.FastSet
+    (
+    -- * Data type
+      FastSet
+    -- * Construction
+    , fromList
+    , set
+    -- * Lookup
+    , member
+    -- * Handy interface
+    , charClass
+    ) where
+
+import Data.Bits ((.|.), (.&.), shiftR)
+import Data.Function (on)
+import Data.List (sort, sortBy)
+import qualified Data.Array.Base as AB
+import qualified Data.Array.Unboxed as A
+import qualified Data.Text as T
+
+data FastSet = FastSet {
+    table :: {-# UNPACK #-} !(A.UArray Int Int)
+  , mask  :: {-# UNPACK #-} !Int
+  }
+
+data Entry = Entry {
+    key          :: {-# UNPACK #-} !Char
+  , initialIndex :: {-# UNPACK #-} !Int
+  , index        :: {-# UNPACK #-} !Int
+  }
+
+offset :: Entry -> Int
+offset e = index e - initialIndex e
+
+resolveCollisions :: [Entry] -> [Entry]
+resolveCollisions [] = []
+resolveCollisions [e] = [e]
+resolveCollisions (a:b:entries) = a' : resolveCollisions (b' : entries)
+  where (a', b')
+          | index a < index b   = (a, b)
+          | offset a < offset b = (b { index=index a }, a { index=index a + 1 })
+          | otherwise           = (a, b { index=index a + 1 })
+
+pad :: Int -> [Entry] -> [Entry]
+pad = go 0
+  where -- ensure that we pad enough so that lookups beyond the
+        -- last hash in the table fall within the array
+        go !_ !m []          = replicate (max 1 m + 1) empty
+        go  k  m (e:entries) = map (const empty) [k..i - 1] ++ e :
+                               go (i + 1) (m + i - k - 1) entries
+          where i            = index e
+        empty                = Entry '\0' maxBound 0
+
+nextPowerOf2 :: Int -> Int
+nextPowerOf2 0  = 1
+nextPowerOf2 x  = go (x - 1) 1
+  where go y 32 = y + 1
+        go y k  = go (y .|. (y `shiftR` k)) $ k * 2
+
+fastHash :: Char -> Int
+fastHash = fromEnum
+
+fromList :: String -> FastSet
+fromList s = FastSet (AB.listArray (0, length interleaved - 1) interleaved)
+             mask'
+  where s'      = ordNub (sort s)
+        l       = length s'
+        mask'   = nextPowerOf2 ((5 * l) `div` 4) - 1
+        entries = pad mask' .
+                  resolveCollisions .
+                  sortBy (compare `on` initialIndex) .
+                  zipWith (\c i -> Entry c i i) s' .
+                  map ((.&. mask') . fastHash) $ s'
+        interleaved = concatMap (\e -> [fromEnum $ key e, initialIndex e])
+                      entries
+
+ordNub :: Eq a => [a] -> [a]
+ordNub []     = []
+ordNub (y:ys) = go y ys
+  where go x (z:zs)
+          | x == z    = go x zs
+          | otherwise = x : go z zs
+        go x []       = [x]
+
+set :: T.Text -> FastSet
+set = fromList . T.unpack
+
+-- | Check the set for membership.
+member :: Char -> FastSet -> Bool
+member c a           = go (2 * i)
+  where i            = fastHash c .&. mask a
+        lookupAt j b = (i' <= i) && (c == c' || b)
+            where c' = toEnum $ AB.unsafeAt (table a) j
+                  i' = AB.unsafeAt (table a) $ j + 1
+        go j         = lookupAt j . lookupAt (j + 2) . lookupAt (j + 4) .
+                       lookupAt (j + 6) . go $ j + 8
+
+charClass :: String -> FastSet
+charClass = fromList . go
+  where go (a:'-':b:xs) = [a..b] ++ go xs
+        go (x:xs)       = x : go xs
+        go _            = ""
diff --git a/tests/QC/Buffer.hs b/tests/QC/Buffer.hs
--- a/tests/QC/Buffer.hs
+++ b/tests/QC/Buffer.hs
@@ -47,11 +47,19 @@
 t_unbuffer :: BPT -> Property
 t_unbuffer (BP _ts t buf) = t === BT.unbuffer buf
 
+-- This test triggers both branches in Data.Attoparsec.Text.Buffer.append
+-- and checks that Data.Text.Array.copyI manipulations are correct.
+t_unbuffer_three :: Property
+t_unbuffer_three = t_unbuffer $ toBP BT.buffer [t, t, t]
+  where
+    -- Make it long enough to increase chances of a segmentation fault
+    t = T.replicate 1000 "\0"
+
 b_length :: BPB -> Property
 b_length (BP _ts t buf) = B.length t === BB.length buf
 
 t_length :: BPT -> Property
-t_length (BP _ts t buf) = T.lengthWord16 t === BT.length buf
+t_length (BP _ts t buf) = BT.lengthCodeUnits t === BT.length buf
 
 b_unsafeIndex :: BPB -> Gen Property
 b_unsafeIndex (BP _ts t buf) = do
@@ -61,14 +69,14 @@
 
 t_iter :: BPT -> Gen Property
 t_iter (BP _ts t buf) = do
-  let l = T.lengthWord16 t
+  let l = BT.lengthCodeUnits t
   i <- choose (0,l-1)
   let it (T.Iter c q) = (c,q)
   return $ l === 0 .||. it (T.iter t i) === it (BT.iter buf i)
 
 t_iter_ :: BPT -> Gen Property
 t_iter_ (BP _ts t buf) = do
-  let l = T.lengthWord16 t
+  let l = BT.lengthCodeUnits t
   i <- choose (0,l-1)
   return $ l === 0 .||. T.iter_ t i === BT.iter_ buf i
 
@@ -77,20 +85,27 @@
   i <- choose (0, B.length t)
   return $ B.unsafeDrop i t === BB.unsafeDrop i buf
 
-t_dropWord16 :: BPT -> Gen Property
-t_dropWord16 (BP _ts t buf) = do
-  i <- choose (0, T.lengthWord16 t)
-  return $ T.dropWord16 i t === BT.dropWord16 i buf
+t_dropCodeUnits :: BPT -> Gen Property
+t_dropCodeUnits (BP _ts t buf) = do
+  i <- choose (0, BT.lengthCodeUnits t)
+  return $ dropCodeUnits i t === BT.dropCodeUnits i buf
+  where
+#if MIN_VERSION_text(2,0,0)
+    dropCodeUnits = T.dropWord8
+#else
+    dropCodeUnits = T.dropWord16
+#endif
 
 tests :: [TestTree]
 tests = [
     testProperty "b_unbuffer" b_unbuffer
   , testProperty "t_unbuffer" t_unbuffer
+  , testProperty "t_unbuffer_three" t_unbuffer_three
   , testProperty "b_length" b_length
   , testProperty "t_length" t_length
   , testProperty "b_unsafeIndex" b_unsafeIndex
   , testProperty "t_iter" t_iter
   , testProperty "t_iter_" t_iter_
   , testProperty "b_unsafeDrop" b_unsafeDrop
-  , testProperty "t_dropWord16" t_dropWord16
+  , testProperty "t_dropCodeUnits" t_dropCodeUnits
   ]
diff --git a/tests/QC/ByteString.hs b/tests/QC/ByteString.hs
--- a/tests/QC/ByteString.hs
+++ b/tests/QC/ByteString.hs
@@ -26,9 +26,11 @@
 satisfy :: Word8 -> L.ByteString -> Property
 satisfy w s = parseBS (P.satisfy (<=w)) (L.cons w s) === Just w
 
-satisfyWith :: Char -> L.ByteString -> Property
-satisfyWith c s = parseBS (P.satisfyWith (chr . fromIntegral) (<=c))
+satisfyWith :: Word8 -> L.ByteString -> Property
+satisfyWith w s = parseBS (P.satisfyWith (chr . fromIntegral) (<=c))
                          (L.cons (fromIntegral (ord c)) s) === Just c
+  where
+    c = chr (fromIntegral w)
 
 word8 :: Word8 -> L.ByteString -> Property
 word8 w s = parseBS (P.word8 w) (L.cons w s) === Just w
@@ -119,6 +121,18 @@
          PL.Done t' h' -> t === t' .&&. toStrictBS h === h'
          _             -> property False
 
+takeWhileIncluding :: Word8 -> L.ByteString -> Property
+takeWhileIncluding w s =
+    let s'    = L.cons w $ L.snoc s (w+1)
+        (h_,t_) = L.span (<=w) s'
+        (h,t) =
+          case L.uncons t_ of
+            Nothing -> (h_, t_)
+            Just (n, nt) -> (h_ `L.snoc` n, nt)
+    in w < 255 ==> case PL.parse (P.takeWhileIncluding (<=w)) s' of
+         PL.Done t' h' -> t === t' .&&. toStrictBS h === h'
+         _             -> property False
+
 takeTill :: Word8 -> L.ByteString -> Property
 takeTill w s =
     let (h,t) = L.break (==w) s
@@ -129,6 +143,19 @@
 takeWhile1_empty :: Property
 takeWhile1_empty = parseBS (P.takeWhile1 undefined) L.empty === Nothing
 
+getChunk :: L.ByteString -> Property
+getChunk s =
+  maybe (property False) (=== L.toChunks s) $
+    parseBS getChunks s
+  where getChunks = go []
+        go res = do
+          mchunk <- P.getChunk
+          case mchunk of
+            Nothing -> return (reverse res)
+            Just chunk -> do
+              _ <- P.take (B.length chunk)
+              go (chunk:res)
+
 endOfInput :: L.ByteString -> Property
 endOfInput s = parseBS P.endOfInput s === if L.null s
                                           then Just ()
@@ -179,6 +206,8 @@
     , testProperty "takeWhile" takeWhile
     , testProperty "takeWhile1" takeWhile1
     , testProperty "takeWhile1_empty" takeWhile1_empty
+    , testProperty "takeWhileIncluding" takeWhileIncluding
+    , testProperty "getChunk" getChunk
     , testProperty "word8" word8
     , testProperty "members" members
     , testProperty "nonmembers" nonmembers
diff --git a/tests/QC/Text.hs b/tests/QC/Text.hs
--- a/tests/QC/Text.hs
+++ b/tests/QC/Text.hs
@@ -6,6 +6,7 @@
 import Control.Applicative ((<*>), (<$>))
 #endif
 import Data.Int (Int64)
+import Data.Word (Word8)
 import Prelude hiding (take, takeWhile)
 import QC.Common (liftOp, parseT)
 import qualified QC.Text.FastSet as FastSet
@@ -16,8 +17,10 @@
 import qualified Data.Attoparsec.Text as P
 import qualified Data.Attoparsec.Text.Lazy as PL
 import qualified Data.Attoparsec.Text.FastSet as S
+import qualified Data.ByteString as BS
 import qualified Data.Char as Char
 import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
 import qualified Data.Text.Lazy as L
 
 -- Basic byte-level combinators.
@@ -70,9 +73,12 @@
     === Just t'
   where t' = toStrict t
 
-stringCI :: T.Text -> Property
-stringCI s = P.parseOnly (P.stringCI fs) s === Right s
+-- | Note: "simple, and efficient" works for well formed input...
+-- i.e. e.g. Latin1 texts
+stringCI :: [Word8] -> Property
+stringCI ws = P.parseOnly (P.stringCI fs) s === Right s
   where fs = T.toCaseFold s
+        s  = TE.decodeLatin1 (BS.pack ws)
 
 asciiCI :: T.Text -> Gen Bool
 asciiCI x =
@@ -150,7 +156,9 @@
                      (c, fst <$> L.uncons s') === ('\r', Just '\n')
 
 scan :: L.Text -> Positive Int64 -> Property
-scan s (Positive k) = parseT p s === Just (toStrict $ L.take k s)
+-- for some reason, if counterexample is removed, this test fails?
+scan s (Positive k) = counterexample (show s)
+                    $ parseT p s === Just (toStrict $ L.take k s)
   where p = P.scan k $ \ n _ ->
             if n > 0 then let !n' = n - 1 in Just n' else Nothing
 
