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,157 +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.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/Data/Attoparsec/ByteString/FastSet.hs b/Data/Attoparsec/ByteString/FastSet.hs
deleted file mode 100644
--- a/Data/Attoparsec/ByteString/FastSet.hs
+++ /dev/null
@@ -1,112 +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 ((.&.), (.|.), 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/Data/Attoparsec/Internal/Compat.hs b/Data/Attoparsec/Internal/Compat.hs
deleted file mode 100644
--- a/Data/Attoparsec/Internal/Compat.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-{-# 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/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.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/Data/Attoparsec/Text/Buffer.hs b/Data/Attoparsec/Text/Buffer.hs
deleted file mode 100644
--- a/Data/Attoparsec/Text/Buffer.hs
+++ /dev/null
@@ -1,231 +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
-    , lengthCodeUnits
-    , dropCodeUnits
-    ) 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(..))
-#if MIN_VERSION_text(2,0,0)
-import Data.Text.Internal.Encoding.Utf8 (utf8LengthByLeader)
-import Data.Text.Unsafe (iterArray, lengthWord8)
-#else
-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
-  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
-#if MIN_VERSION_text(2,0,0)
-      A.copyI newlen 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 newlen 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/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/attoparsec.cabal b/attoparsec.cabal
--- a/attoparsec.cabal
+++ b/attoparsec.cabal
@@ -1,12 +1,12 @@
 name:            attoparsec
-version:         0.14.3
+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>, Ben Gamari <ben@smart-cactus.org>
 stability:       experimental
-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.4, GHC == 9.0.1
+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:   2.0
 homepage:        https://github.com/bgamari/attoparsec
@@ -18,8 +18,6 @@
     file formats.
 extra-source-files:
     README.markdown
-    benchmarks/*.cabal
-    benchmarks/*.hs
     benchmarks/*.txt
     benchmarks/json-data/*.json
     benchmarks/Makefile
@@ -28,15 +26,31 @@
     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.3 && < 5,
@@ -44,9 +58,10 @@
                  containers,
                  deepseq,
                  scientific >= 0.3.1 && < 0.4,
-                 transformers >= 0.2 && (< 0.4 || >= 0.4.1.0) && < 0.6,
+                 transformers >= 0.2 && (< 0.4 || >= 0.4.1.0) && < 0.7,
                  text >= 1.1.1.3,
-                 ghc-prim <0.9
+                 ghc-prim <0.9,
+                 attoparsec-internal
   if impl(ghc < 7.4)
     build-depends:
       bytestring < 0.10.4.0
@@ -54,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.20
+                   semigroups >=0.16.1 && <0.21
 
   exposed-modules: Data.Attoparsec
                    Data.Attoparsec.ByteString
@@ -70,13 +85,7 @@
                    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.Compat
-                   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
 
@@ -86,9 +95,9 @@
     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
@@ -102,25 +111,6 @@
                   QC.Text.FastSet
                   QC.Text.Regressions
 
-  other-modules:  Data.Attoparsec.ByteString
-                  Data.Attoparsec.ByteString.Buffer
-                  Data.Attoparsec.ByteString.Char8
-                  Data.Attoparsec.ByteString.FastSet
-                  Data.Attoparsec.ByteString.Internal
-                  Data.Attoparsec.ByteString.Lazy
-                  Data.Attoparsec.Combinator
-                  Data.Attoparsec.Internal
-                  Data.Attoparsec.Internal.Compat
-                  Data.Attoparsec.Internal.Fhthagn
-                  Data.Attoparsec.Internal.Types
-                  Data.Attoparsec.Number
-                  Data.Attoparsec.Text
-                  Data.Attoparsec.Text.Buffer
-                  Data.Attoparsec.Text.FastSet
-                  Data.Attoparsec.Text.Internal
-                  Data.Attoparsec.Text.Lazy
-                  Data.Attoparsec.Zepto
-
   ghc-options:
     -Wall -threaded -rtsopts
 
@@ -129,6 +119,8 @@
 
   build-depends:
     array,
+    attoparsec,
+    attoparsec-internal,
     base,
     bytestring,
     deepseq >= 1.1,
@@ -148,13 +140,15 @@
     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
@@ -172,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,
@@ -184,6 +179,7 @@
     http-types,
     parsec >= 3.1.2,
     scientific,
+    tasty-bench >= 0.3,
     text >= 1.1.1.0,
     transformers,
     unordered-containers,
@@ -199,4 +195,3 @@
 source-repository head
   type:     git
   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,8 +1,7 @@
 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
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/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,3 +1,9 @@
+# 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
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,6 +47,14 @@
 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
 
@@ -92,6 +100,7 @@
 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
