diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,97 @@
+# streaming-bytestring
+
+## 0.3.4 (2025-02-11)
+
+#### Changed
+
+- Ensure support for GHC 9.12.
+
+## 0.3.3 (2024-10-01)
+
+#### Fixed
+
+- A subtle bug involving conversion to lazy bytestrings.
+
+## 0.3.2 (2023-11-17)
+
+#### Changed
+
+- Ensure support for GHC 9.8.
+
+## 0.3.1 (2023-06-28)
+
+#### Changed
+
+- Ensure support for GHC 9.6.
+
+## 0.3.0 (2023-04-24)
+
+#### Changed
+
+- Dropped support for GHC 7.
+- Tightened PVP version bounds, for GHC 8.0 through to GHC 9.4.4.
+
+## 0.2.4 (2022-08-26)
+
+#### Changed
+
+- Changed `for`'s callback to return `ByteStream m x`, to clarify that it is not
+  used.
+
+## 0.2.3 (2022-08-18)
+
+#### Added
+
+- Add `for :: Monad m => ByteStream m r -> (P.ByteString -> ByteStream m r) -> ByteStream m r`
+
+## 0.2.2 (2022-05-18)
+
+#### Changed
+
+- Dependency adjustments.
+
+## 0.2.1 (2021-06-23)
+
+#### Changed
+
+- Performance improvement when using GHC 9.
+
+## 0.2.0 (2020-10-26)
+
+**Note:** The deprecations added in `0.1.7` have _not_ been removed in this
+version. Instead of `0.1.7`, that release should have been `0.2` in the first
+place.
+
+#### Added
+
+- Add missing exports of `zipWithStream`, `materialize`, and `dematerialize`.
+
+#### Changed
+
+- **Breaking:** Switched names of `fold` and `fold_` in the non-`Char8` modules.
+  The corresponding `Char8` functions and the rest of the library uses `_` for
+  the variant that forgets the `r` value.
+- **Breaking:** Unified `nextByte`/`nextChar` with `uncons`. The old `uncons`
+  returned `Maybe` instead of the more natural `Either r`.
+- **Breaking:** Similarly, `unconsChunk` and `nextChunk` have been unified.
+- `nextByte`, `nextChar`, and `nextChunk` have been deprecated.
+- Relaxed signature of `toStrict_` to allow any `r`, not just `()`.
+- Permance improvements for `packChars` and `denull`.
+- Various documentation improvements.
+- Improved performance of `w8IsSpace` to more quickly filter out non-whitespace
+  characters, and updated `words` to use it instead of the internal function
+  `isSpaceWord8` from the `bytestring` package. See also
+  [bytestring#315](https://github.com/haskell/bytestring/pull/315).
+
+#### Fixed
+
+- An edge case involving overflow in `readInt`.
+- A potential crash in `uncons`.
+- `intersperse` now ignores any initial empty chunks.
+- `intercalate` now does not insert anything between the final substream and the
+  outer stream end.
+- `unlines` now correctly handles `Chunk "" (Empty r)` and `Empty r`.
+
 ## 0.1.7 (2020-10-14)
 
 Thanks to Viktor Dukhovni and Colin Woodbury for their contributions to this release.
diff --git a/lib/Streaming/ByteString.hs b/lib/Streaming/ByteString.hs
--- a/lib/Streaming/ByteString.hs
+++ b/lib/Streaming/ByteString.hs
@@ -1,7 +1,9 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE CPP          #-}
 {-# LANGUAGE GADTs        #-}
+{-# LANGUAGE LambdaCase   #-}
 {-# LANGUAGE RankNTypes   #-}
+{-# LANGUAGE ViewPatterns #-}
 
 -- |
 -- Module      : Streaming.ByteString
@@ -51,79 +53,81 @@
   , ByteString
 
     -- * Introducing and eliminating 'ByteStream's
-  , empty            -- empty :: ByteStream m ()
-  , singleton        -- singleton :: Monad m => Word8 -> ByteStream m ()
-  , pack             -- pack :: Monad m => Stream (Of Word8) m r -> ByteStream m r
-  , unpack           -- unpack :: Monad m => ByteStream m r -> Stream (Of Word8) m r
-  , fromLazy         -- fromLazy :: Monad m => ByteString -> ByteStream m ()
-  , toLazy           -- toLazy :: Monad m => ByteStream m () -> m ByteString
-  , toLazy_          -- toLazy' :: Monad m => ByteStream m () -> m (Of ByteString r)
-  , fromChunks       -- fromChunks :: Monad m => Stream (Of ByteString) m r -> ByteStream m r
-  , toChunks         -- toChunks :: Monad m => ByteStream m r -> Stream (Of ByteString) m r
-  , fromStrict       -- fromStrict :: ByteString -> ByteStream m ()
-  , toStrict         -- toStrict :: Monad m => ByteStream m () -> m ByteString
-  , toStrict_        -- toStrict_ :: Monad m => ByteStream m r -> m (Of ByteString r)
+  , empty
+  , singleton
+  , pack
+  , unpack
+  , fromLazy
+  , toLazy
+  , toLazy_
+  , fromChunks
+  , toChunks
+  , fromStrict
+  , toStrict
+  , toStrict_
   , effects
   , copy
   , drained
   , mwrap
-  , distribute       -- distribute :: ByteStream (t m) a -> t (ByteStream m) a
 
     -- * Transforming ByteStreams
-  , map              -- map :: Monad m => (Word8 -> Word8) -> ByteStream m r -> ByteStream m r
-  , intercalate      -- intercalate :: Monad m => ByteStream m () -> Stream (ByteStream m) m r -> ByteStream m r
-  , intersperse      -- intersperse :: Monad m => Word8 -> ByteStream m r -> ByteStream m r
+  , map
+  , for
+  , intercalate
+  , intersperse
 
     -- * Basic interface
-  , cons             -- cons :: Monad m => Word8 -> ByteStream m r -> ByteStream m r
-  , cons'            -- cons' :: Word8 -> ByteStream m r -> ByteStream m r
+  , cons
+  , cons'
   , snoc
-  , append           -- append :: Monad m => ByteStream m r -> ByteStream m s -> ByteStream m s
-  , filter           -- filter :: (Word8 -> Bool) -> ByteStream m r -> ByteStream m r
-  , uncons           -- uncons :: Monad m => ByteStream m r -> m (Either r (Word8, ByteStream m r))
-  , nextByte -- nextByte :: Monad m => ByteStream m r -> m (Either r (Word8, ByteStream m r))
-  , denull
+  , append
+  , filter
+  , uncons
+  , nextByte
 
     -- * Substrings
     -- ** Breaking strings
-  , break            -- break :: Monad m => (Word8 -> Bool) -> ByteStream m r -> ByteStream m (ByteStream m r)
-  , drop             -- drop :: Monad m => GHC.Int.Int64 -> ByteStream m r -> ByteStream m r
+  , break
+  , drop
   , dropWhile
-  , group            -- group :: Monad m => ByteStream m r -> Stream (ByteStream m) m r
+  , group
   , groupBy
-  , span             -- span :: Monad m => (Word8 -> Bool) -> ByteStream m r -> ByteStream m (ByteStream m r)
-  , splitAt          -- splitAt :: Monad m => GHC.Int.Int64 -> ByteStream m r -> ByteStream m (ByteStream m r)
-  , splitWith        -- splitWith :: Monad m => (Word8 -> Bool) -> ByteStream m r -> Stream (ByteStream m) m r
-  , take             -- take :: Monad m => GHC.Int.Int64 -> ByteStream m r -> ByteStream m ()
-  , takeWhile        -- takeWhile :: (Word8 -> Bool) -> ByteStream m r -> ByteStream m ()
+  , span
+  , splitAt
+  , splitWith
+  , take
+  , takeWhile
 
     -- ** Breaking into many substrings
-  , split            -- split :: Monad m => Word8 -> ByteStream m r -> Stream (ByteStream m) m r
+  , split
 
     -- ** Special folds
-  , concat          -- concat :: Monad m => Stream (ByteStream m) m r -> ByteStream m r
+  , concat
+  , denull
 
     -- * Builders
-  , toStreamingByteStringWith
   , toStreamingByteString
+
+  , toStreamingByteStringWith
+
   , toBuilder
   , concatBuilders
 
     -- * Building ByteStreams
     -- ** Infinite ByteStreams
-  , repeat           -- repeat :: Word8 -> ByteStream m r
-  , iterate          -- iterate :: (Word8 -> Word8) -> Word8 -> ByteStream m r
-  , cycle            -- cycle :: Monad m => ByteStream m r -> ByteStream m s
+  , repeat
+  , iterate
+  , cycle
 
     -- ** Unfolding ByteStreams
-  , unfoldM          -- unfoldr :: (a -> m (Maybe (Word8, a))) -> m a -> ByteStream m ()
-  , unfoldr          -- unfold  :: (a -> Either r (Word8, a)) -> a -> ByteStream m r
+  , unfoldM
+  , unfoldr
   , reread
 
     -- *  Folds, including support for `Control.Foldl`
-  , foldr            -- foldr :: Monad m => (Word8 -> a -> a) -> a -> ByteStream m () -> m a
-  , fold             -- fold :: Monad m => (x -> Word8 -> x) -> x -> (x -> b) -> ByteStream m () -> m b
-  , fold_            -- fold' :: Monad m => (x -> Word8 -> x) -> x -> (x -> b) -> ByteStream m r -> m (b, r)
+  , foldr
+  , fold
+  , fold_
   , head
   , head_
   , last
@@ -139,29 +143,27 @@
 
     -- * I\/O with 'ByteStream's
     -- ** Standard input and output
-  , getContents      -- getContents :: ByteStream IO ()
-  , stdin            -- stdin :: ByteStream IO ()
-  , stdout           -- stdout :: ByteStream IO r -> IO r
-  , interact         -- interact :: (ByteStream IO () -> ByteStream IO r) -> IO r
+  , getContents
+  , stdin
+  , stdout
+  , interact
 
     -- ** Files
-  , readFile         -- readFile :: FilePath -> ByteStream IO ()
-  , writeFile        -- writeFile :: FilePath -> ByteStream IO r -> IO r
-  , appendFile       -- appendFile :: FilePath -> ByteStream IO r -> IO r
+  , readFile
+  , writeFile
+  , appendFile
 
     -- ** I\/O with Handles
-  , fromHandle       -- fromHandle :: Handle -> ByteStream IO ()
-  , toHandle         -- toHandle :: Handle -> ByteStream IO r -> IO r
-  , hGet             -- hGet :: Handle -> Int -> ByteStream IO ()
-  , hGetContents     -- hGetContents :: Handle -> ByteStream IO ()
-  , hGetContentsN    -- hGetContentsN :: Int -> Handle -> ByteStream IO ()
-  , hGetN            -- hGetN :: Int -> Handle -> Int -> ByteStream IO ()
-  , hGetNonBlocking  -- hGetNonBlocking :: Handle -> Int -> ByteStream IO ()
-  , hGetNonBlockingN -- hGetNonBlockingN :: Int -> Handle -> Int -> ByteStream IO ()
-  , hPut             -- hPut :: Handle -> ByteStream IO r -> IO r
-  --    , hPutNonBlocking  -- hPutNonBlocking :: Handle -> ByteStream IO r -> ByteStream IO r
-    -- * Etc.
-  , zipWithStream    -- zipWithStream :: Monad m => (forall x. a -> ByteStream m x -> ByteStream m x) -> [a] -> Stream (ByteStream m) m r -> Stream (ByteStream m) m r
+  , fromHandle
+  , toHandle
+  , hGet
+  , hGetContents
+  , hGetContentsN
+  , hGetN
+  , hGetNonBlocking
+  , hGetNonBlockingN
+  , hPut
+  --    , hPutNonBlocking
 
     -- * Simple chunkwise operations
   , unconsChunk
@@ -174,6 +176,12 @@
   , chunkMap
   , chunkMapM
   , chunkMapM_
+
+    -- * Etc.
+  , dematerialize
+  , materialize
+  , distribute
+  , zipWithStream
   ) where
 
 import           Prelude hiding
@@ -181,11 +189,11 @@
     elem, filter, foldl, foldl1, foldr, foldr1, getContents, getLine, head,
     init, interact, iterate, last, length, lines, map, maximum, minimum,
     notElem, null, putStr, putStrLn, readFile, repeat, replicate, reverse,
-    scanl, scanl1, scanr, scanr1, span, splitAt, tail, take, takeWhile,
-    unlines, unzip, writeFile, zip, zipWith)
+    scanl, scanl1, scanr, scanr1, span, splitAt, tail, take, takeWhile, unlines,
+    unzip, writeFile, zip, zipWith)
 
-import qualified Data.ByteString as P (ByteString)
 import qualified Data.ByteString as B
+import qualified Data.ByteString as P (ByteString)
 import           Data.ByteString.Builder.Internal hiding
     (append, defaultChunkSize, empty, hPut)
 import qualified Data.ByteString.Internal as B
@@ -202,11 +210,10 @@
 import           Data.Int (Int64)
 import qualified Data.List as L
 import           Data.Word (Word8)
-import           Foreign.ForeignPtr (withForeignPtr)
 import           Foreign.Ptr
 import           Foreign.Storable
-import           System.IO (Handle, IOMode(..), hClose, openBinaryFile)
 import qualified System.IO as IO (stdin, stdout)
+import           System.IO (Handle, IOMode(..), hClose, openBinaryFile)
 import           System.IO.Error (illegalOperationErrorType, mkIOError)
 
 -- | /O(n)/ Concatenate a stream of byte streams.
@@ -286,8 +293,12 @@
 -- Note that this is an /expensive/ operation that forces the whole monadic
 -- ByteString into memory and then copies all the data. If possible, try to
 -- avoid converting back and forth between streaming and strict bytestrings.
-toStrict_ :: Monad m => ByteStream m () -> m B.ByteString
+toStrict_ :: Monad m => ByteStream m r -> m B.ByteString
+#if MIN_VERSION_streaming (0,2,2)
 toStrict_ = fmap B.concat . SP.toList_ . toChunks
+#else
+toStrict_ = fmap B.concat . SP.toList_ . void . toChunks
+#endif
 {-# INLINE toStrict_ #-}
 
 -- | /O(n)/ Convert a monadic byte stream into a single strict 'ByteString',
@@ -318,10 +329,10 @@
 {-# INLINE fromLazy #-}
 
 -- | /O(n)/ Convert an effectful byte stream into a single lazy 'ByteStream'
--- with the same internal chunk structure. See `toLazy` which preserve
--- connectedness by keeping the return value of the effectful bytestring.
+-- with the same internal chunk structure. See `toLazy` which preserves
+-- connectness by keeping the return value of the effectful bytestring.
 toLazy_ :: Monad m => ByteStream m r -> m BI.ByteString
-toLazy_ bs = dematerialize bs (\_ -> return BI.Empty) (fmap . BI.Chunk) join
+toLazy_ bs = dematerialize bs (\_ -> return BI.Empty) (fmap . BI.chunk) join
 {-# INLINE toLazy_ #-}
 
 -- | /O(n)/ Convert an effectful byte stream into a single lazy 'ByteString'
@@ -346,7 +357,7 @@
                 (\r -> return (BI.Empty :> r))
                 (\b mx -> do
                       (bs :> x) <- mx
-                      return $ BI.Chunk b bs :> x
+                      return $ BI.chunk b bs :> x
                       )
                 join
 {-# INLINE toLazy #-}
@@ -410,15 +421,44 @@
 
 -- | Remove empty ByteStrings from a stream of bytestrings.
 denull :: Monad m => Stream (ByteStream m) m r -> Stream (ByteStream m) m r
-denull = hoist (run . maps effects) . separate . mapped nulls
-{-# INLINE denull #-}
+{-# INLINABLE denull #-}
+denull = loop . Right
+  where
+    -- Scan each substream, dropping empty chunks along the way.  As soon as a
+    -- non-empty chunk is found, just apply the loop to the next substream in
+    -- the terminal value via fmap.  If Empty comes up before that happens,
+    -- continue the current stream instead with its denulled tail.
+    --
+    -- This implementation is tail recursive:
+    -- * Recursion via 'loop . Left' continues scanning an inner ByteStream.
+    -- * Recursion via 'loop . Right' moves to the next substream.
+    --
+    -- The old version below was shorter, but noticeably slower, especially
+    -- when empty substreams are frequent:
+    --
+    --    denull = hoist (run . maps effects) . separate . mapped nulls
+    --
+    loop = \ case
+      Left mbs -> case mbs of
+          Chunk c cs | B.length c > 0 -> Step $ Chunk c $ fmap (loop . Right) cs
+                     | otherwise      -> loop $ Left cs
+          Go m                        -> Effect $ loop . Left <$> m
+          Empty r                     -> loop $ Right r
+      Right strm -> case strm of
+        Step mbs -> case mbs of
+          Chunk c cs | B.length c > 0 -> Step $ Chunk c $ fmap (loop . Right) cs
+                     | otherwise      -> loop $ Left cs
+          Go m                        -> Effect $ loop . Left <$> m
+          Empty r                     -> loop $ Right r
+        Effect m                      -> Effect $ fmap (loop . Right) m
+        r@(Return _)                  -> r
 
 {-| /O1/ Distinguish empty from non-empty lines, while maintaining streaming;
     the empty ByteStrings are on the right
 
 >>> nulls  ::  ByteStream m r -> m (Sum (ByteStream m) (ByteStream m) r)
 
-    There are many ways to remove null bytestrings from a
+    There are many (generally slower) ways to remove null bytestrings from a
     @Stream (ByteStream m) m r@ (besides using @denull@). If we pass next to
 
 >>> mapped nulls bs :: Stream (Sum (ByteStream m) (ByteStream m)) m r
@@ -521,49 +561,40 @@
 head (Go m)      = m >>= head
 {-# INLINABLE head #-}
 
--- | /O(1)/ Extract the head and tail of a 'ByteStream', or 'Nothing' if it is
--- empty.
-uncons :: Monad m => ByteStream m r -> m (Maybe (Word8, ByteStream m r))
-uncons (Empty _) = return Nothing
-uncons (Chunk c cs)
-    = return $ Just (B.unsafeHead c
-                     , if B.length c == 1
-                         then cs
-                         else Chunk (B.unsafeTail c) cs )
-uncons (Go m) = m >>= uncons
-{-# INLINABLE uncons #-}
-
 -- | /O(1)/ Extract the head and tail of a 'ByteStream', or its return value if
 -- it is empty. This is the \'natural\' uncons for an effectful byte stream.
+uncons :: Monad m => ByteStream m r -> m (Either r (Word8, ByteStream m r))
+uncons (Chunk c@(B.length -> len) cs)
+    | len > 0    = let !h = B.unsafeHead c
+                       !t = if len > 1 then Chunk (B.unsafeTail c) cs else cs
+                    in return $ Right (h, t)
+    | otherwise  = uncons cs
+uncons (Go m)    = m >>= uncons
+uncons (Empty r) = return (Left r)
+{-# INLINABLE uncons #-}
+
+-- | The same as `uncons`, will be removed in the next version.
 nextByte :: Monad m => ByteStream m r -> m (Either r (Word8, ByteStream m r))
-nextByte (Empty r) = return (Left r)
-nextByte (Chunk c cs)
-    = if B.null c
-        then nextByte cs
-        else return $ Right (B.unsafeHead c
-                     , if B.length c == 1
-                         then cs
-                         else Chunk (B.unsafeTail c) cs )
-nextByte (Go m) = m >>= nextByte
+nextByte = uncons
 {-# INLINABLE nextByte #-}
+{-# DEPRECATED nextByte "Use uncons instead." #-}
 
 -- | Like `uncons`, but yields the entire first `B.ByteString` chunk that the
--- stream is holding onto. If there wasn't one, it tries to fetch it.
-unconsChunk :: Monad m => ByteStream m r -> m (Maybe (B.ByteString, ByteStream m r))
-unconsChunk (Empty _)    = return Nothing
-unconsChunk (Chunk c cs) = return (Just (c,cs))
-unconsChunk (Go m)       = m >>= unconsChunk
+-- stream is holding onto. If there wasn't one, it tries to fetch it.  Yields
+-- the final @r@ return value when the 'ByteStream' is empty.
+unconsChunk :: Monad m => ByteStream m r -> m (Either r (B.ByteString, ByteStream m r))
+unconsChunk (Chunk c cs)
+  | B.null c = unconsChunk cs
+  | otherwise = return (Right (c,cs))
+unconsChunk (Go m) = m >>= unconsChunk
+unconsChunk (Empty r) = return (Left r)
 {-# INLINABLE unconsChunk #-}
 
--- | Similar to `unconsChunk`, but yields the final @r@ return value when there
--- is no subsequent chunk.
+-- | The same as `unconsChunk`, will be removed in the next version.
 nextChunk :: Monad m => ByteStream m r -> m (Either r (B.ByteString, ByteStream m r))
-nextChunk (Empty r) = return (Left r)
-nextChunk (Go m) = m >>= nextChunk
-nextChunk (Chunk c cs)
-  | B.null c = nextChunk cs
-  | otherwise = return (Right (c,cs))
+nextChunk = unconsChunk
 {-# INLINABLE nextChunk #-}
+{-# DEPRECATED nextChunk "Use unconsChunk instead." #-}
 
 -- | /O(n\/c)/ Extract the last element of a 'ByteStream', which must be finite
 -- and non-empty.
@@ -609,6 +640,20 @@
 map f z = dematerialize z Empty (Chunk . B.map f) Go
 {-# INLINE map #-}
 
+-- | @'for' xs f@ applies @f@ to each chunk in the stream, and
+-- concatenates the resulting streams.
+--
+-- Generalised in 0.2.4 to match @streaming@: the callback's (ignored)
+-- return value can be of any type.
+--
+-- @since 0.2.3
+for :: Monad m => ByteStream m r -> (P.ByteString -> ByteStream m x) -> ByteStream m r
+for stream f = case stream of
+  Empty r      -> Empty r
+  Chunk bs bss -> f bs *> for bss f
+  Go m         -> Go ((`for` f) <$> m)
+{-# INLINE for #-}
+
 -- -- | /O(n)/ 'reverse' @xs@ returns the elements of @xs@ in reverse order.
 -- reverse :: ByteString -> ByteString
 -- reverse cs0 = rev Empty cs0
@@ -622,49 +667,49 @@
 intersperse :: Monad m => Word8 -> ByteStream m r -> ByteStream m r
 intersperse _ (Empty r)    = Empty r
 intersperse w (Go m)       = Go (fmap (intersperse w) m)
-intersperse w (Chunk c cs) = Chunk (B.intersperse w c)
-                                   (dematerialize cs Empty (Chunk . intersperse') Go)
+intersperse w (Chunk c cs) | B.null c = intersperse w cs
+                           | otherwise =
+                               Chunk (B.intersperse w c)
+                                 (dematerialize cs Empty (Chunk . intersperse') Go)
   where intersperse' :: P.ByteString -> P.ByteString
-        intersperse' (B.PS fp o l) =
-          B.unsafeCreate (2*l) $ \p' -> withForeignPtr fp $ \p -> do
-            poke p' w
-            B.c_intersperse (p' `plusPtr` 1) (p `plusPtr` o) (fromIntegral l) w
-
+        intersperse' (B.PS fp o l)
+          | l > 0 = B.unsafeCreate (2*l) $ \p' -> unsafeWithForeignPtr fp $ \p -> do
+              poke p' w
+              B.c_intersperse (p' `plusPtr` 1) (p `plusPtr` o) (fromIntegral l) w
+          | otherwise = B.empty
 {-# INLINABLE intersperse #-}
 
 -- | 'foldr', applied to a binary operator, a starting value (typically the
 -- right-identity of the operator), and a ByteStream, reduces the ByteStream
 -- using the binary operator, from right to left.
 --
--- > foldr cons = id
---
 foldr :: Monad m => (Word8 -> a -> a) -> a -> ByteStream m () -> m a
 foldr k = foldrChunks (flip (B.foldr k))
 {-# INLINE foldr #-}
 
--- | 'fold', applied to a binary operator, a starting value (typically the
+-- | 'fold_', applied to a binary operator, a starting value (typically the
 -- left-identity of the operator), and a ByteStream, reduces the ByteStream
 -- using the binary operator, from left to right. We use the style of the foldl
--- libarary for left folds
-fold :: Monad m => (x -> Word8 -> x) -> x -> (x -> b) -> ByteStream m () -> m b
-fold step0 begin finish p0 = loop p0 begin
+-- library for left folds
+fold_ :: Monad m => (x -> Word8 -> x) -> x -> (x -> b) -> ByteStream m () -> m b
+fold_ step0 begin finish p0 = loop p0 begin
   where
     loop p !x = case p of
         Chunk bs bss -> loop bss $! B.foldl' step0 x bs
         Go    m      -> m >>= \p' -> loop p' x
         Empty _      -> return (finish x)
-{-# INLINABLE fold #-}
+{-# INLINABLE fold_ #-}
 
--- | 'fold_' keeps the return value of the left-folded bytestring. Useful for
+-- | 'fold' keeps the return value of the left-folded bytestring. Useful for
 -- simultaneous folds over a segmented bytestream.
-fold_ :: Monad m => (x -> Word8 -> x) -> x -> (x -> b) -> ByteStream m r -> m (Of b r)
-fold_ step0 begin finish p0 = loop p0 begin
+fold :: Monad m => (x -> Word8 -> x) -> x -> (x -> b) -> ByteStream m r -> m (Of b r)
+fold step0 begin finish p0 = loop p0 begin
   where
     loop p !x = case p of
         Chunk bs bss -> loop bss $! B.foldl' step0 x bs
         Go    m      -> m >>= \p' -> loop p' x
         Empty r      -> return (finish x :> r)
-{-# INLINABLE fold_ #-}
+{-# INLINABLE fold #-}
 
 -- ---------------------------------------------------------------------
 -- Special folds
@@ -786,10 +831,9 @@
 >>> Q.putStrLn $ Q.drop 6 "Wisconsin"
 sin
 >>> Q.putStrLn $ Q.drop 16 "Wisconsin"
-
->>>
+<BLANKLINE>
 -}
-drop  :: Monad m => Int64 -> ByteStream m r -> ByteStream m r
+drop :: Monad m => Int64 -> ByteStream m r -> ByteStream m r
 drop i p | i <= 0 = p
 drop i cs0 = drop' i cs0
   where drop' 0 cs           = cs
@@ -975,20 +1019,13 @@
 -- 'ByteStream's and concatenates the list after interspersing the first
 -- argument between each element of the list.
 intercalate :: Monad m => ByteStream m () -> Stream (ByteStream m) m r -> ByteStream m r
-intercalate _ (Return r) = Empty r
-intercalate s (Effect m) = Go $ fmap (intercalate s) m
-intercalate s (Step bs0) = do  -- this isn't quite right
-  ls <- bs0
-  s
-  intercalate s ls
- -- where
- --  loop (Return r) =  Empty r -- concat . (L.intersperse s)
- --  loop (Effect m) = Go $ fmap loop m
- --  loop (Step bs) = do
- --    ls <- bs
- --    case ls of
- --      Return r -> Empty r  -- no '\n' before end, in this case.
- --      x -> s >> loop x
+intercalate s = loop
+  where
+    loop (Return r) = Empty r
+    loop (Effect m) = Go $ fmap loop m
+    loop (Step bs) = bs >>= \case
+        Return r -> Empty r  -- not between final substream and stream end
+        x        -> s >> loop x
 {-# INLINABLE intercalate #-}
 
 -- | Returns the number of times its argument appears in the `ByteStream`.
@@ -1250,8 +1287,8 @@
 
 -- | Zip a list and a stream-of-byte-streams together.
 zipWithStream
-  :: (Monad m)
-  =>  (forall x . a -> ByteStream m x -> ByteStream m x)
+  :: Monad m
+  => (forall x . a -> ByteStream m x -> ByteStream m x)
   -> [a]
   -> Stream (ByteStream m) m r
   -> Stream (ByteStream m) m r
@@ -1271,8 +1308,7 @@
 -- 哈斯克尔 98
 --
 -- <https://gist.github.com/michaelt/6ea89ca95a77b0ef91f3 This benchmark> shows
--- its indistinguishable performance is indistinguishable from
--- @toLazyByteString@
+-- its performance is indistinguishable from @toLazyByteString@
 toStreamingByteString :: MonadIO m => Builder -> ByteStream m ()
 toStreamingByteString = toStreamingByteStringWith
  (safeStrategy BI.smallChunkSize BI.defaultChunkSize)
@@ -1307,9 +1343,9 @@
 concatBuilders :: Stream (Of Builder) IO () -> Builder
 concatBuilders p = builder $ \bstep r -> do
   case p of
-    Return _          -> runBuilderWith mempty bstep r
-    Step (b :> rest)  -> runBuilderWith (b `mappend` concatBuilders rest) bstep r
-    Effect m          -> m >>= \p' -> runBuilderWith (concatBuilders p') bstep r
+    Return _         -> runBuilderWith mempty bstep r
+    Step (b :> rest) -> runBuilderWith (b `mappend` concatBuilders rest) bstep r
+    Effect m         -> m >>= \p' -> runBuilderWith (concatBuilders p') bstep r
 {-# INLINABLE concatBuilders #-}
 
 -- | A simple construction of a builder from a 'ByteString'.
diff --git a/lib/Streaming/ByteString/Char8.hs b/lib/Streaming/ByteString/Char8.hs
--- a/lib/Streaming/ByteString/Char8.hs
+++ b/lib/Streaming/ByteString/Char8.hs
@@ -4,6 +4,7 @@
 {-# LANGUAGE OverloadedStrings   #-}
 {-# LANGUAGE RankNTypes          #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE ViewPatterns        #-}
 
 -- |
 -- Module      : Streaming.ByteString.Char8
@@ -26,20 +27,20 @@
   , ByteString
 
     -- * Introducing and eliminating 'ByteStream's
-  , empty            -- empty :: ByteStream m ()
-  , pack             -- pack :: Monad m => String -> ByteStream m ()
+  , empty
+  , pack
   , unpack
   , string
   , unlines
   , unwords
-  , singleton        -- singleton :: Monad m => Char -> ByteStream m ()
-  , fromChunks       -- fromChunks :: Monad m => Stream (Of ByteString) m r -> ByteStream m r
-  , fromLazy         -- fromLazy :: Monad m => ByteString -> ByteStream m ()
-  , fromStrict       -- fromStrict :: ByteString -> ByteStream m ()
-  , toChunks         -- toChunks :: Monad m => ByteStream m r -> Stream (Of ByteString) m r
-  , toLazy           -- toLazy :: Monad m => ByteStream m () -> m ByteString
+  , singleton
+  , fromChunks
+  , fromLazy
+  , fromStrict
+  , toChunks
+  , toLazy
   , toLazy_
-  , toStrict         -- toStrict :: Monad m => ByteStream m () -> m ByteString
+  , toStrict
   , toStrict_
   , effects
   , copy
@@ -47,72 +48,74 @@
   , mwrap
 
     -- * Transforming ByteStreams
-  , map              -- map :: Monad m => (Char -> Char) -> ByteStream m r -> ByteStream m r
-  , intercalate      -- intercalate :: Monad m => ByteStream m () -> Stream (ByteStream m) m r -> ByteStream m r
-  , intersperse      -- intersperse :: Monad m => Char -> ByteStream m r -> ByteStream m r
+  , map
+  , intercalate
+  , intersperse
 
     -- * Basic interface
-  , cons             -- cons :: Monad m => Char -> ByteStream m r -> ByteStream m r
-  , cons'            -- cons' :: Char -> ByteStream m r -> ByteStream m r
+  , cons
+  , cons'
   , snoc
-  , append           -- append :: Monad m => ByteStream m r -> ByteStream m s -> ByteStream m s
-  , filter           -- filter :: (Char -> Bool) -> ByteStream m r -> ByteStream m r
-  , head             -- head :: Monad m => ByteStream m r -> m Char
-  , head_            -- head' :: Monad m => ByteStream m r -> m (Of Char r)
-  , last             -- last :: Monad m => ByteStream m r -> m Char
-  , last_            -- last' :: Monad m => ByteStream m r -> m (Of Char r)
-  , null             -- null :: Monad m => ByteStream m r -> m Bool
+  , append
+  , filter
+  , head
+  , head_
+  , last
+  , last_
+  , null
   , null_
+  , nulls
   , testNull
-  , nulls            -- null' :: Monad m => ByteStream m r -> m (Of Bool r)
-  , uncons           -- uncons :: Monad m => ByteStream m r -> m (Either r (Char, ByteStream m r))
+  , uncons
   , nextChar
   , skipSomeWS
 
     -- * Substrings
     -- ** Breaking strings
-  , break            -- break :: Monad m => (Char -> Bool) -> ByteStream m r -> ByteStream m (ByteStream m r)
-  , drop             -- drop :: Monad m => GHC.Int.Int64 -> ByteStream m r -> ByteStream m r
+  , break
+  , drop
   , dropWhile
-  , group            -- group :: Monad m => ByteStream m r -> Stream (ByteStream m) m r
+  , group
   , groupBy
-  , span             -- span :: Monad m => (Char -> Bool) -> ByteStream m r -> ByteStream m (ByteStream m r)
-  , splitAt          -- splitAt :: Monad m => GHC.Int.Int64 -> ByteStream m r -> ByteStream m (ByteStream m r)
-  , splitWith        -- splitWith :: Monad m => (Char -> Bool) -> ByteStream m r -> Stream (ByteStream m) m r
-  , take             -- take :: Monad m => GHC.Int.Int64 -> ByteStream m r -> ByteStream m ()
-  , takeWhile        -- takeWhile :: (Char -> Bool) -> ByteStream m r -> ByteStream m ()
+  , span
+  , splitAt
+  , splitWith
+  , take
+  , takeWhile
 
     -- ** Breaking into many substrings
-  , split            -- split :: Monad m => Char -> ByteStream m r -> Stream (ByteStream m) m r
+  , split
   , lines
-  , words
   , lineSplit
-  , denull
+  , words
 
     -- ** Special folds
-  , concat          -- concat :: Monad m => Stream (ByteStream m) m r -> ByteStream m r
+  , concat
+  , denull
 
     -- * Builders
   , toStreamingByteString
+
   , toStreamingByteStringWith
+
   , toBuilder
   , concatBuilders
 
     -- * Building ByteStreams
     -- ** Infinite ByteStreams
-  , repeat           -- repeat :: Char -> ByteStream m ()
-  , iterate          -- iterate :: (Char -> Char) -> Char -> ByteStream m ()
-  , cycle            -- cycle :: Monad m => ByteStream m r -> ByteStream m s
+  , repeat
+  , iterate
+  , cycle
 
     -- ** Unfolding ByteStreams
-  , unfoldr          -- unfoldr :: (a -> Maybe (Char, a)) -> a -> ByteStream m ()
-  , unfoldM          -- unfold  :: (a -> Either r (Char, a)) -> a -> ByteStream m r
+  , unfoldr
+  , unfoldM
   , reread
 
     -- *  Folds, including support for `Control.Foldl`
---    , foldr            -- foldr :: Monad m => (Char -> a -> a) -> a -> ByteStream m () -> m a
-  , fold             -- fold :: Monad m => (x -> Char -> x) -> x -> (x -> b) -> ByteStream m () -> m b
-  , fold_            -- fold' :: Monad m => (x -> Char -> x) -> x -> (x -> b) -> ByteStream m r -> m (b, r)
+    -- , foldr
+  , fold
+  , fold_
   , length
   , length_
   , count
@@ -121,29 +124,29 @@
 
     -- * I\/O with 'ByteStream's
     -- ** Standard input and output
-  , getContents      -- getContents :: ByteStream IO ()
-  , stdin            -- stdin :: ByteStream IO ()
-  , stdout           -- stdout :: ByteStream IO r -> IO r
-  , interact         -- interact :: (ByteStream IO () -> ByteStream IO r) -> IO r
+  , getContents
+  , stdin
+  , stdout
+  , interact
   , putStr
   , putStrLn
 
     -- ** Files
-  , readFile         -- readFile :: FilePath -> ByteStream IO ()
-  , writeFile        -- writeFile :: FilePath -> ByteStream IO r -> IO r
-  , appendFile       -- appendFile :: FilePath -> ByteStream IO r -> IO r
+  , readFile
+  , writeFile
+  , appendFile
 
     -- ** I\/O with Handles
-  , fromHandle       -- fromHandle :: Handle -> ByteStream IO ()
-  , toHandle         -- toHandle :: Handle -> ByteStream IO r -> IO r
-  , hGet             -- hGet :: Handle -> Int -> ByteStream IO ()
-  , hGetContents     -- hGetContents :: Handle -> ByteStream IO ()
-  , hGetContentsN    -- hGetContentsN :: Int -> Handle -> ByteStream IO ()
-  , hGetN            -- hGetN :: Int -> Handle -> Int -> ByteStream IO ()
-  , hGetNonBlocking  -- hGetNonBlocking :: Handle -> Int -> ByteStream IO ()
-  , hGetNonBlockingN -- hGetNonBlockingN :: Int -> Handle -> Int -> ByteStream IO ()
-  , hPut             -- hPut :: Handle -> ByteStream IO r -> IO r
---    , hPutNonBlocking  -- hPutNonBlocking :: Handle -> ByteStream IO r -> ByteStream IO r
+  , fromHandle
+  , toHandle
+  , hGet
+  , hGetContents
+  , hGetContentsN
+  , hGetN
+  , hGetNonBlocking
+  , hGetNonBlockingN
+  , hPut
+    -- , hPutNonBlocking
 
     -- * Simple chunkwise operations
   , unconsChunk
@@ -158,10 +161,10 @@
   , chunkMapM_
 
     -- * Etc.
---    , zipWithStream    -- zipWithStream :: Monad m => (forall x. a -> ByteStream m x -> ByteStream m x) -> [a] -> Stream (ByteStream m) m r -> Stream (ByteStream m) m r
-  , distribute      -- distribute :: ByteStream (t m) a -> t (ByteStream m) a
-  , materialize
   , dematerialize
+  , materialize
+  , distribute
+  , zipWithStream
   ) where
 
 import           Prelude hiding
@@ -194,16 +197,16 @@
     length_, nextChunk, null, null_, nulls, readFile, splitAt, stdin, stdout,
     take, testNull, toBuilder, toChunks, toHandle, toLazy, toLazy_,
     toStreamingByteString, toStreamingByteStringWith, toStrict, toStrict_,
-    unconsChunk, writeFile)
+    unconsChunk, writeFile, zipWithStream)
 
+import           Data.Bits ((.&.))
 import           Data.Word (Word8)
-import           Foreign.ForeignPtr (withForeignPtr)
 import           Foreign.Ptr
 import           Foreign.Storable
 import qualified System.IO as IO
 
 -- | Given a stream of bytes, produce a vanilla `Stream` of characters.
-unpack ::  Monad m => ByteStream m r ->  Stream (Of Char) m r
+unpack :: Monad m => ByteStream m r -> Stream (Of Char) m r
 unpack bs = case bs of
     Empty r    -> Return r
     Go m       -> Effect (fmap unpack m)
@@ -218,7 +221,7 @@
 
   unpackAppendCharsStrict :: B.ByteString -> Stream (Of Char) m r -> Stream (Of Char) m r
   unpackAppendCharsStrict (B.PS fp off len) xs =
-    B.accursedUnutterablePerformIO $ withForeignPtr fp $ \base -> do
+    B.accursedUnutterablePerformIO $ unsafeWithForeignPtr fp $ \base -> do
          loop (base `plusPtr` (off-1)) (base `plusPtr` (off-1+len)) xs
      where
        loop !sentinal !p acc
@@ -303,18 +306,24 @@
 groupBy rel = Q.groupBy (\w w' -> rel (w2c w) (w2c w'))
 {-# INLINE groupBy #-}
 
--- | /O(1)/ Extract the head and tail of a ByteStream, returning Nothing
--- if it is empty.
+-- | /O(1)/ Extract the head and tail of a 'ByteStream', or its return value if
+-- it is empty. This is the \'natural\' uncons for an effectful byte stream.
 uncons :: Monad m => ByteStream m r -> m (Either r (Char, ByteStream m r))
+uncons (Chunk c@(B.length -> len) cs)
+    | len > 0    = let !h = w2c $ B.unsafeHead c
+                       !t = if len > 1 then Chunk (B.unsafeTail c) cs else cs
+                    in return $ Right (h, t)
+    | otherwise  = uncons cs
+uncons (Go m)    = m >>= uncons
 uncons (Empty r) = return (Left r)
-uncons (Chunk c cs)
-    = return $ Right (w2c (B.unsafeHead c)
-                     , if B.length c == 1
-                         then cs
-                         else Chunk (B.unsafeTail c) cs )
-uncons (Go m) = m >>= uncons
 {-# INLINABLE uncons #-}
 
+-- | The same as `uncons`, will be removed in the next version.
+nextChar :: Monad m => ByteStream m r -> m (Either r (Char, ByteStream m r))
+nextChar = uncons
+{-# INLINABLE nextChar #-}
+{-# DEPRECATED nextChar "Use uncons instead." #-}
+
 -- ---------------------------------------------------------------------
 -- Transformations
 
@@ -514,17 +523,13 @@
 --  the \"lines\" had embedded newlines.
 --
 --  * @'unlines' . 'lines'@ will replace @\\r\\n@ with @\\n@.
-unlines :: Monad m => Stream (ByteStream m) m r ->  ByteStream m r
+unlines :: Monad m => Stream (ByteStream m) m r -> ByteStream m r
 unlines = loop where
   loop str =  case str of
     Return r -> Empty r
     Step bstr   -> do
       st <- bstr
-      let bs = unlines st
-      case bs of
-        Chunk "" (Empty r)   -> Empty r
-        Chunk "\n" (Empty _) -> bs
-        _                    -> cons' '\n' bs
+      cons' '\n' $ unlines st
     Effect m  -> Go (fmap unlines m)
 {-# INLINABLE unlines #-}
 
@@ -536,7 +541,7 @@
 -- can write @mapped toStrict . words@ or the like, if strict bytestrings are
 -- needed.
 words :: Monad m => ByteStream m r -> Stream (ByteStream m) m r
-words = filtered . Q.splitWith B.isSpaceWord8
+words = filtered . Q.splitWith w8IsSpace
  where
   filtered stream = case stream of
     Return r -> Return r
@@ -622,7 +627,7 @@
            -> Int            -- remaining number of newlines wanted
            -> Either Int Int -- Left count, else Right length
 nthNewLine (B.PS fp off len) targetLines =
-    B.accursedUnutterablePerformIO $ withForeignPtr fp $ \base ->
+    B.accursedUnutterablePerformIO $ unsafeWithForeignPtr fp $ \base ->
     loop (base `plusPtr` off) targetLines 0 len
   where
     loop :: Ptr Word8 -> Int -> Int -> Int -> IO (Either Int Int)
@@ -663,15 +668,6 @@
 count c = Q.count (c2w c)
 {-# INLINE count #-}
 
--- | /O(1)/ Extract the head and tail of a 'ByteStream', or its return value if
--- it is empty. This is the \'natural\' uncons for an effectful byte stream.
-nextChar :: Monad m => ByteStream m r -> m (Either r (Char, ByteStream m r))
-nextChar b = do
-  e <- Q.nextByte b
-  case e of
-    Left r       -> return $! Left r
-    Right (w,bs) -> return $! Right (w2c w, bs)
-
 -- | Print a stream of bytes to STDOUT.
 putStr :: MonadIO m => ByteStream m r -> m r
 putStr = hPut IO.stdout
@@ -702,10 +698,11 @@
     -- the conversion from Word8 to Word is free.
     let w :: Word
         !w = fromIntegral w8
-     in w - 0x21 > 0x7e   -- not [x21..0x9f]
-        && ( w == 0x20    -- SP
-          || w - 0x09 < 5 -- HT, NL, VT, FF, CR
-          || w == 0xa0 )  -- NBSP
+     in w .&. 0x50 == 0    -- Quick non-wsp filter
+        && w - 0x21 > 0x7e -- 2nd non-wsp filter
+        && ( w == 0x20     -- SP
+          || w - 0x09 < 5  -- HT, NL, VT, FF, CR
+          || w == 0xa0 )   -- NBSP
 {-# INLINE w8IsSpace #-}
 
 -- | Try to position the stream at the next non-whitespace input, by
@@ -730,14 +727,14 @@
     go _ r                      = r
 
 -- | Try to read an 'Int' value from the 'ByteString', returning
--- @m (Compose -- (Just val :> str))@ on success, where @val@ is the
--- value read and @str@ is the rest of the input stream.  If the stream
--- of digits decodes to a value larger than can be represented by an
--- 'Int', the returned value will be @m (Compose (Nothing :> str))@,
--- where the content of @str@ is the same as the original stream, but
--- some of the monadic effects may already have taken place, so the
--- original stream MUST NOT be used.  To read the remaining data, you
--- MUST use the returned @str@.
+-- @m (Compose (Just val :> str))@ on success, where @val@ is the value
+-- read and @str@ is the rest of the input stream.  If the stream of
+-- digits decodes to a value larger than can be represented by an 'Int',
+-- the returned value will be @m (Compose (Nothing :> str))@, where the
+-- content of @str@ is the same as the original stream, but some of the
+-- monadic effects may already have taken place, so the original stream
+-- MUST NOT be used.  To read the remaining data, you MUST use the
+-- returned @str@.
 --
 -- This function will not read an /unreasonably/ long stream of leading
 -- zero digits when trying to decode a number.  When reading the first
@@ -795,9 +792,13 @@
             Chunk c cs
                 | !l <- B.length c
                 , l > 0 -> case accumWord acc c of
-                     (0, !_, !_)
+                     (0, !_, !inrange)
+                         | inrange
                            -- no more digits found
                            -> result nbytes acc str
+                         | otherwise
+                           -- Overlow on first digit of chunk
+                           -> overflow nbytes acc str
                      (!n, !a, !inrange)
                          | False <- inrange
                            -- result out of 'Int' range
@@ -821,7 +822,7 @@
         -- the provided digits (end of input or non-digit encountered).
         accumWord acc (B.PS fp off len) =
             B.accursedUnutterablePerformIO $ do
-                withForeignPtr fp $ \p -> do
+                unsafeWithForeignPtr fp $ \p -> do
                     let ptr = p `plusPtr` off
                         end = ptr `plusPtr` len
                     x@(!_, !_, !_) <- if positive
diff --git a/lib/Streaming/ByteString/Internal.hs b/lib/Streaming/ByteString/Internal.hs
--- a/lib/Streaming/ByteString/Internal.hs
+++ b/lib/Streaming/ByteString/Internal.hs
@@ -5,6 +5,7 @@
 {-# LANGUAGE MagicHash             #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TypeOperators         #-}
 {-# LANGUAGE UnboxedTuples         #-}
 {-# LANGUAGE UndecidableInstances  #-}
 {-# LANGUAGE UnliftedFFITypes      #-}
@@ -19,16 +20,16 @@
 module Streaming.ByteString.Internal
   ( ByteStream(..)
   , ByteString
-  , consChunk         -- :: ByteString -> ByteStream m r -> ByteStream m r
-  , chunkOverhead     -- :: Int
-  , defaultChunkSize  -- :: Int
-  , materialize       -- :: (forall x. (r -> x) -> (ByteString -> x -> x) -> (m x -> x) -> x) -> ByteStream m r
-  , dematerialize     -- :: Monad m =>  ByteStream m r -> forall x.  (r -> x) -> (ByteString -> x -> x) -> (m x -> x) -> x
-  , foldrChunks       -- :: Monad m =>  (ByteString -> a -> a) -> a -> ByteStream m r -> m a
-  , foldlChunks       -- :: Monad m =>  (a -> ByteString -> a) -> a -> ByteStream m r -> m a
+  , consChunk
+  , chunkOverhead
+  , defaultChunkSize
+  , materialize
+  , dematerialize
+  , foldrChunks
+  , foldlChunks
 
-  , foldrChunksM      -- :: Monad m => (ByteString -> m a -> m a) -> m a -> ByteStream m r -> m a
-  , foldlChunksM      -- :: Monad m => (ByteString -> m a -> m a) -> m a -> ByteStream m r -> m a
+  , foldrChunksM
+  , foldlChunksM
   , chunkFold
   , chunkFoldM
   , chunkMap
@@ -38,10 +39,10 @@
   , unfoldrChunks
 
   , packChars
-  , smallChunkSize   -- :: Int
-  , unpackBytes      -- :: Monad m => ByteStream m r -> Stream (Of Word8) m r
   , packBytes
-  , chunk            -- :: ByteString -> ByteStream m ()
+  , unpackBytes
+  , chunk
+  , smallChunkSize
   , mwrap
   , unfoldrNE
   , reread
@@ -52,6 +53,9 @@
 
     -- * ResourceT help
   , bracketByteString
+
+    -- * Re-export from GHC 9.0
+  , unsafeWithForeignPtr
   ) where
 
 import           Control.Monad
@@ -78,21 +82,32 @@
 import qualified Streaming.Prelude as SP
 
 import           Data.String
-import           Foreign.ForeignPtr (withForeignPtr)
 import           Foreign.Ptr
 import           Foreign.Storable
-import           GHC.Exts (SpecConstrAnnotation(..))
+import           GHC.Types (SPEC(..))
 
 import           Data.Functor.Identity
 import           Data.Word
 import           GHC.Base (realWorld#)
 import           GHC.IO (IO(IO))
-import           System.IO.Unsafe
+import           System.IO.Unsafe (unsafePerformIO)
 
 import           Control.Monad.Base
 import           Control.Monad.Catch (MonadCatch(..))
 import           Control.Monad.Trans.Resource
 
+#if MIN_VERSION_base(4,15,0)
+import           GHC.ForeignPtr (unsafeWithForeignPtr)
+#else
+import           Foreign.ForeignPtr (ForeignPtr, withForeignPtr)
+#endif
+
+#if !MIN_VERSION_base(4,15,0)
+-- | Synonym of 'withForeignPtr' for GHC prior to 9.0.
+unsafeWithForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b
+unsafeWithForeignPtr = withForeignPtr
+#endif
+
 -- | A type alias for back-compatibility.
 type ByteString = ByteStream
 {-# DEPRECATED ByteString "Use ByteStream instead." #-}
@@ -119,18 +134,18 @@
   {-# INLINE pure #-}
   bf <*> bx = do {f <- bf; x <- bx; Empty (f x)}
   {-# INLINE (<*>) #-}
-  (*>) = (>>)
-  {-# INLINE (*>) #-}
-
-instance Monad m => Monad (ByteStream m) where
-  return = Empty
-  {-# INLINE return #-}
-  x0 >> y = loop SPEC x0 where
+  x0 *> y = loop SPEC x0 where
     loop !_ x = case x of   -- this seems to be insanely effective
       Empty _   -> y
       Chunk a b -> Chunk a (loop SPEC b)
       Go m      -> Go (fmap (loop SPEC) m)
-  {-# INLINEABLE (>>) #-}
+  {-# INLINEABLE (*>) #-}
+
+instance Monad m => Monad (ByteStream m) where
+  return = pure
+  {-# INLINE return #-}
+  (>>) = (*>)
+  {-# INLINE (>>) #-}
   x >>= f =
     -- case x of
     --   Empty a -> f a
@@ -175,7 +190,11 @@
 instance (Monoid r, Monad m) => Monoid (ByteStream m r) where
   mempty = Empty mempty
   {-# INLINE mempty #-}
+#if MIN_VERSION_base(4,11,0)
+  mappend = (<>)
+#else
   mappend = liftM2 mappend
+#endif
   {-# INLINE mappend #-}
 
 instance (MonadBase b m) => MonadBase b (ByteStream m) where
@@ -215,9 +234,6 @@
         Chunk bs rest -> Chunk bs (loop rest)
 {-# INLINABLE bracketByteString #-}
 
-data SPEC = SPEC | SPEC2
-{-# ANN type SPEC ForceSpecConstr #-}
-
 -- -- ------------------------------------------------------------------------
 --
 -- | Smart constructor for 'Chunk'.
@@ -234,7 +250,7 @@
 
 
 {- | Reconceive an effect that results in an effectful bytestring as an effectful bytestring.
-    Compare Streaming.mwrap. The closes equivalent of
+    Compare Streaming.mwrap. The closest equivalent of
 
 >>> Streaming.wrap :: f (Stream f m r) -> Stream f m r
 
@@ -316,14 +332,17 @@
 --             assert (l' <= l) $ return (B.PS fp 0 l', res)
 -- {-# INLINABLE packBytes' #-}
 
+-- | Convert a `Stream` of pure `Word8` into a chunked 'ByteStream'.
 packBytes :: Monad m => Stream (Of Word8) m r -> ByteStream m r
 packBytes cs0 = do
+  -- XXX: Why 32?  It seems like a rather small chunk size, wouldn't
+  -- smallChunkSize make a better choice?
   (bytes :> rest) <- lift $ SP.toList $ SP.splitAt 32 cs0
   case bytes of
     [] -> case rest of
       Return r -> Empty r
       Step as  -> packBytes (Step as)  -- these two pattern matches
-      Effect m -> Go $ fmap packBytes m -- should be evaded.
+      Effect m -> Go $ fmap packBytes m -- should be avoided.
     _  -> Chunk (B.packBytes bytes) (packBytes rest)
 {-# INLINABLE packBytes #-}
 
@@ -331,7 +350,21 @@
 --
 -- /Note:/ Each `Char` value is truncated to 8 bits.
 packChars :: Monad m => Stream (Of Char) m r -> ByteStream m r
-packChars = packBytes . SP.map B.c2w
+packChars str = do
+  -- XXX: Why 32?  It seems like a rather small chunk size, wouldn't
+  -- smallChunkSize make a better choice?
+  --
+  -- We avoid the cost of converting the stream of Chars to a stream
+  -- of Word8 (passed to packBytes), and instead pass the original
+  -- `Char` arrays to 'B.packChars', which will be more efficient,
+  -- the conversion there will be essentially free.
+  (chars :> rest) <- lift $ SP.toList $ SP.splitAt 32 str
+  case chars of
+    [] -> case rest of
+      Return r -> Empty r
+      Step as  -> packChars (Step as)  -- these two pattern matches
+      Effect m -> Go $ fmap packChars m -- should be avoided.
+    _  -> Chunk (B.packChars chars) (packChars rest)
 {-# INLINABLE packChars #-}
 
 -- | The reverse of `packChars`. Given a stream of bytes, produce a `Stream`
@@ -340,28 +373,28 @@
 unpackBytes bss = dematerialize bss Return unpackAppendBytesLazy Effect
   where
   unpackAppendBytesLazy :: B.ByteString -> Stream (Of Word8) m r -> Stream (Of Word8) m r
-  unpackAppendBytesLazy (B.PS fp off len) xs
-    | len <= 100 = unpackAppendBytesStrict (B.PS fp off len) xs
+  unpackAppendBytesLazy b@(B.PS fp off len) xs
+    | len <= 100 = unpackAppendBytesStrict b xs
     | otherwise  = unpackAppendBytesStrict (B.PS fp off 100) remainder
     where
       remainder  = unpackAppendBytesLazy (B.PS fp (off+100) (len-100)) xs
 
   unpackAppendBytesStrict :: B.ByteString -> Stream (Of Word8) m r -> Stream (Of Word8) m r
   unpackAppendBytesStrict (B.PS fp off len) xs =
-    B.accursedUnutterablePerformIO $ withForeignPtr fp $ \base ->
+    B.accursedUnutterablePerformIO $ unsafeWithForeignPtr fp $ \base ->
       loop (base `plusPtr` (off-1)) (base `plusPtr` (off-1+len)) xs
     where
-      loop !sentinal !p acc
-        | p == sentinal = return acc
+      loop !sentinel !p acc
+        | p == sentinel = return acc
         | otherwise     = do
             x <- peek p
-            loop sentinal (p `plusPtr` (-1)) (Step (x :> acc))
+            loop sentinel (p `plusPtr` (-1)) (Step (x :> acc))
 {-# INLINABLE unpackBytes #-}
 
 -- | Copied from Data.ByteString.Unsafe for compatibility with older bytestring.
 unsafeLast :: B.ByteString -> Word8
 unsafeLast (B.PS x s l) =
-    accursedUnutterablePerformIO $ withForeignPtr x $ \p -> peekByteOff p (s+l-1)
+    accursedUnutterablePerformIO $ unsafeWithForeignPtr x $ \p -> peekByteOff p (s+l-1)
  where
       accursedUnutterablePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
 {-# INLINE unsafeLast #-}
@@ -478,16 +511,18 @@
       Right (bs,s') -> return $ Chunk bs (loop s')
 {-# INLINABLE unfoldrChunks #-}
 
--- | Stream chunks from something that contains @IO (Maybe ByteString)@ until it
+-- | Stream chunks from something that contains @m (Maybe ByteString)@ until it
 -- returns 'Nothing'. 'reread' is of particular use rendering @io-streams@ input
 -- streams as byte streams in the present sense.
 --
--- > Q.reread Streams.read            :: InputStream B.ByteString -> Q.ByteString IO ()
--- > Q.reread (liftIO . Streams.read) :: MonadIO m => InputStream B.ByteString -> Q.ByteString m ()
+-- > import qualified Data.ByteString as B
+-- > import qualified System.IO.Streams as S
+-- > Q.reread S.read            :: S.InputStream B.ByteString -> Q.ByteStream IO ()
+-- > Q.reread (liftIO . S.read) :: MonadIO m => S.InputStream B.ByteString -> Q.ByteStream m ()
 --
 -- The other direction here is
 --
--- > Streams.unfoldM Q.unconsChunk    :: Q.ByteString IO r -> IO (InputStream B.ByteString)
+-- > S.unfoldM Q.unconsChunk    :: Q.ByteString IO r -> IO (S.InputStream B.ByteString)
 reread :: Monad m => (s -> m (Maybe B.ByteString)) -> s -> ByteStream m ()
 reread step s = loop where
   loop = Go $ do
@@ -525,8 +560,9 @@
 world!
 >>> :! cat hello1.txt
 hello
-
+<BLANKLINE>
 world
+<BLANKLINE>
 
     As with the parallel operations in @Streaming.Prelude@, we have
 
@@ -551,7 +587,7 @@
 findIndexOrEnd :: (Word8 -> Bool) -> B.ByteString -> Int
 findIndexOrEnd k (B.PS x s l) =
     B.accursedUnutterablePerformIO $
-      withForeignPtr x $ \f -> go (f `plusPtr` s) 0
+      unsafeWithForeignPtr x $ \f -> go (f `plusPtr` s) 0
   where
     go !ptr !n | n >= l    = return l
                | otherwise = do w <- peek ptr
diff --git a/streaming-bytestring.cabal b/streaming-bytestring.cabal
--- a/streaming-bytestring.cabal
+++ b/streaming-bytestring.cabal
@@ -1,6 +1,6 @@
 cabal-version:      >=1.10
 name:               streaming-bytestring
-version:            0.1.7
+version:            0.3.4
 synopsis:           Fast, effectful byte streams.
 description:
   This library enables fast and safe streaming of byte data, in either @Word8@ or
@@ -34,17 +34,21 @@
   https://github.com/haskell-streaming/streaming-bytestring/issues
 
 tested-with:
-  GHC ==7.10.3
-   || ==8.0.2
+  GHC ==8.0.2
    || ==8.2.2
    || ==8.4.4
    || ==8.6.5
    || ==8.8.4
-   || ==8.10.2
+   || ==8.10.3
+   || ==9.0.2
+   || ==9.2.8
+   || ==9.4.8
+   || ==9.6.6
+   || ==9.8.4
 
 source-repository head
   type:     git
-  location: https://github.com/michaelt/streaming-bytestring
+  location: https://github.com/haskell-streaming/streaming-bytestring
 
 library
   default-language: Haskell2010
@@ -67,31 +71,21 @@
     Unsafe
 
   build-depends:
-      base               >=4.8     && <5.0
-    , bytestring
-    , deepseq
-    , exceptions
-    , mmorph             >=1.0     && <1.2
-    , mtl                >=2.1     && <2.3
-    , resourcet
+      base               >=4.9     && <5.0
+    , bytestring         >=0.10.4  && <0.13
+    , deepseq            >=1.4     && <1.6
+    , exceptions         >=0.8     && <0.11
+    , ghc-prim           >=0.4     && <0.14
+    , mmorph             >=1.0     && <1.3
+    , mtl                >=2.2     && <2.4
+    , resourcet          >=1.1     && <1.4
     , streaming          >=0.1.4.0 && <0.3
-    , transformers       >=0.3     && <0.6
-    , transformers-base
-
-  if impl(ghc <7.8)
-    build-depends:
-        bytestring          >=0 && <0.10.4.0
-      , bytestring-builder
-
-  else
-    if impl(ghc <8.0)
-      build-depends: bytestring >=0.10.4 && <0.11
-
-    else
-      build-depends: bytestring >=0.10.4 && <0.12
+    , transformers       >=0.4     && <0.7
+    , transformers-base  >=0.4     && <0.5
 
   if impl(ghc <8.0)
-    build-depends: semigroups
+    build-depends:
+      semigroups         >=0.18    && <0.19
 
 test-suite test
   default-language: Haskell2010
@@ -99,13 +93,13 @@
   hs-source-dirs:   tests
   main-is:          Test.hs
   build-depends:
-      base                  >=4        && <5
-    , bytestring
-    , resourcet             >=1.1
+      base                  >=4.9     && <5
+    , bytestring            >=0.10.4  && <0.13
+    , resourcet             >=1.1     && <1.4
     , smallcheck            >=1.1.1
-    , streaming
+    , streaming             >=0.1.4.0 && <0.3
     , streaming-bytestring
     , tasty                 >=0.11.0.4
     , tasty-hunit           >=0.9
     , tasty-smallcheck      >=0.8.1
-    , transformers
+    , transformers          >=0.3     && <0.7
diff --git a/tests/Test.hs b/tests/Test.hs
--- a/tests/Test.hs
+++ b/tests/Test.hs
@@ -5,6 +5,7 @@
 
 import           Control.Monad.Trans.Resource (runResourceT)
 import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString.Lazy
 import qualified Data.ByteString.Lazy.Char8 as BL
 import           Data.Function (on)
 import           Data.Functor.Compose (Compose(..))
@@ -12,8 +13,8 @@
 import qualified Data.IORef as IOR
 import qualified Data.List as L
 import           Data.String (fromString)
-import           Streaming (Of(..))
 import qualified Streaming as SM
+import           Streaming (Of(..))
 import qualified Streaming.ByteString as Q
 import qualified Streaming.ByteString.Char8 as Q8
 import qualified Streaming.ByteString.Internal as QI
@@ -118,6 +119,11 @@
     $ Q8.readFile "tests/groupBy.txt"  -- ByteStream IO ()
   l @?= 57
 
+emptyChunks :: Assertion
+emptyChunks = do
+  bs <- Q.toLazy_ $ QI.chunkMap (const mempty) "bar"
+  assertBool "Expected empty chunks" $ Data.ByteString.Lazy.null bs
+
 readIntCases :: Assertion
 readIntCases = do
   let imax = maxBound :: Int
@@ -314,6 +320,19 @@
        $ addEffect cnt
        $ QI.Empty 16
   check cnt res Nothing (smin10 :> 16)
+  -- maxBound with cross-chunk overflow
+  IOR.writeIORef cnt 4
+  res <- readIntSkip
+       $ QI.Chunk " \t\n\v\f\r\xa0"
+       $ addEffect cnt
+       $ QI.Chunk (B.take 4 smax)
+       $ addEffect cnt
+       $ QI.Chunk (B.drop 4 smax)
+       $ addEffect cnt
+       $ QI.Chunk "00"
+       $ addEffect cnt
+       $ QI.Empty 17
+  check cnt res Nothing (smax `B.append` "00" :> 17)
   where
     -- Count down to zero from initial value
     readIntSkip = Q8.readInt . Q8.skipSomeWS
@@ -357,6 +376,11 @@
         unpackToString (Q8.concat (Q8.lineSplit n (fromString str))) == str
     , testProperty "concat after lineSplit round trips (CRLF)" $ over ((,) <$> nats <~> strSeriesCrlf) $ \(n,str) ->
         unpackToString (Q8.concat (Q8.lineSplit n (fromString str))) == str
+    , testProperty "'for' is equivalent to the one in streaming" $ over chunksSeries $ \ss ->
+        let doubleStream b = S.each [b, b]
+            doubleByteStream b = Q.chunk b *> Q.chunk b
+        in unpackToString (Q.fromChunks (S.for (Q.toChunks (fromChunks ss)) doubleStream))
+           == unpackToString (Q.for (fromChunks ss) doubleByteStream)
     ]
   , testGroup "Unit Tests"
     [ testCase "hGetContents: Handle stays open" handleIsOpen
@@ -366,5 +390,6 @@
     , testCase "findIndexOrEnd" goodFindIndex
     , testCase "Stream Interop" firstI
     , testCase "readInt" readIntCases
+    , testCase "toLazy_: empty chunks" emptyChunks
     ]
   ]
