Commits

Bryan O'Sullivan committed 046b1a9

ZOMG size isn't everything.

  • Participants
  • Parent commits b04eae3

Comments (0)

Files changed (10)

File Data/Text/Encoding/Fusion.hs

 import Data.ByteString as B
 import Data.ByteString.Internal (ByteString(..), mallocByteString, memcpy)
 import Data.Text.Fusion (Step(..), Stream(..))
+import Data.Text.Fusion.Size
 import Data.Text.Encoding.Error
 import Data.Text.Encoding.Fusion.Common
 import Data.Text.UnsafeChar (unsafeChr, unsafeChr8, unsafeChr32)
-import Data.Text.UnsafeShift (shiftL)
+import Data.Text.UnsafeShift (shiftL, shiftR)
 import Data.Word (Word8, Word16, Word32)
 import Foreign.ForeignPtr (withForeignPtr, ForeignPtr)
 import Foreign.Storable (pokeByteOff)
 import qualified Data.Text.Encoding.Utf32 as U32
 
 streamASCII :: ByteString -> Stream Char
-streamASCII bs = Stream next 0 l
+streamASCII bs = Stream next 0 (maxSize l)
     where
       l = B.length bs
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'ByteString' into a 'Stream Char', using UTF-8
 -- encoding.
 streamUtf8 :: OnDecodeError -> ByteString -> Stream Char
-streamUtf8 onErr bs = Stream next 0 l
+streamUtf8 onErr bs = Stream next 0 (maxSize l)
     where
       l = B.length bs
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'ByteString' into a 'Stream Char', using little
 -- endian UTF-16 encoding.
 streamUtf16LE :: OnDecodeError -> ByteString -> Stream Char
-streamUtf16LE onErr bs = Stream next 0 l
+streamUtf16LE onErr bs = Stream next 0 (maxSize (l `shiftR` 1))
     where
       l = B.length bs
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'ByteString' into a 'Stream Char', using big
 -- endian UTF-16 encoding.
 streamUtf16BE :: OnDecodeError -> ByteString -> Stream Char
-streamUtf16BE onErr bs = Stream next 0 l
+streamUtf16BE onErr bs = Stream next 0 (maxSize (l `shiftR` 1))
     where
       l = B.length bs
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'ByteString' into a 'Stream Char', using big
 -- endian UTF-32 encoding.
 streamUtf32BE :: OnDecodeError -> ByteString -> Stream Char
-streamUtf32BE onErr bs = Stream next 0 l
+streamUtf32BE onErr bs = Stream next 0 (maxSize (l `shiftR` 2))
     where
       l = B.length bs
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'ByteString' into a 'Stream Char', using little
 -- endian UTF-32 encoding.
 streamUtf32LE :: OnDecodeError -> ByteString -> Stream Char
-streamUtf32LE onErr bs = Stream next 0 l
+streamUtf32LE onErr bs = Stream next 0 (maxSize (l `shiftR` 2))
     where
       l = B.length bs
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'Stream' 'Word8' to a 'ByteString'.
 unstream :: Stream Word8 -> ByteString
 unstream (Stream next s0 len) = unsafePerformIO $ do
-    mallocByteString len >>= loop len 0 s0
+    let mlen = upperBound 4 len
+    mallocByteString mlen >>= loop mlen 0 s0
     where
       loop !n !off !s fp = case next s of
           Done -> trimUp fp n off

File Data/Text/Fusion.hs

     , countChar
     ) where
 
-import Prelude (Bool(..), Char, Eq(..), Maybe(..), Monad(..), Int,
+import Prelude (Bool(..), Char, Maybe(..), Monad(..), Int,
                 Num(..), Ord(..), ($), (&&),
                 fromIntegral, otherwise)
 import Data.Bits ((.&.))
 import qualified Data.Text.Array as A
 import qualified Data.Text.Fusion.Common as S
 import Data.Text.Fusion.Internal
+import Data.Text.Fusion.Size
 import qualified Data.Text.Internal as I
 import qualified Data.Text.Encoding.Utf16 as U16
 import qualified Prelude as P
 
 -- | /O(n)/ Convert a 'Text' into a 'Stream Char'.
 stream :: Text -> Stream Char
-stream (Text arr off len) = Stream next off len
+stream (Text arr off len) = Stream next off (maxSize len)
     where
       end = off+len
       {-# INLINE next #-}
 -- | /O(n)/ Convert a 'Text' into a 'Stream Char', but iterate
 -- backwards.
 reverseStream :: Text -> Stream Char
-reverseStream (Text arr off len) = Stream next (off+len-1) len
+reverseStream (Text arr off len) = Stream next (off+len-1) (maxSize len)
     where
       {-# INLINE next #-}
       next !i
 -- | /O(n)/ Convert a 'Stream Char' into a 'Text'.
 unstream :: Stream Char -> Text
 unstream (Stream next0 s0 len)
-    | len == 0  = I.empty
-    | otherwise = I.textP (P.fst a) 0 (P.snd a)
+    | isEmpty len = I.empty
+    | otherwise   = I.textP (P.fst a) 0 (P.snd a)
     where
-      a = A.run2 (A.unsafeNew len >>= (\arr -> loop arr len s0 0))
+      mlen = upperBound 4 len
+      a = A.run2 (A.unsafeNew mlen >>= (\arr -> loop arr mlen s0 0))
       loop arr !top !s !i
           | i + 1 >= top = case next0 s of
                             Done -> return (arr, i)
 -- | /O(n)/ Reverse the characters of a string.
 reverse :: Stream Char -> Text
 reverse (Stream next s len0)
-    | len0 == 0 = I.empty
-    | otherwise = I.textP arr off' len'
+    | isEmpty len0 = I.empty
+    | otherwise    = I.textP arr off' len'
   where
-    len0' = max len0 4
+    len0' = upperBound 4 (larger len0 4)
     (arr, (off', len')) = A.run2 (A.unsafeNew len0' >>= loop s (len0'-1) len0')
     loop !s0 !i !len marr =
         case next s0 of

File Data/Text/Fusion/Common.hs

 import Data.Int (Int64)
 import Data.Text.Fusion.Internal
 import Data.Text.Fusion.CaseMapping (foldMapping, lowerMapping, upperMapping)
+import Data.Text.Fusion.Size
 
 singleton :: Char -> Stream Char
-singleton c = Stream next False 1 -- HINT maybe too low
+singleton c = Stream next False 1
     where next False = Yield c True
           next True  = Done
 {-# INLINE singleton #-}
 streamList :: [a] -> Stream a
 {-# INLINE [0] streamList #-}
 streamList [] = empty
-streamList s  = Stream next s unknownLength
+streamList s  = Stream next s unknownSize
     where next []       = Done
           next (x:xs)   = Yield x xs
-          unknownLength = 8 -- random HINT
 
 unstreamList :: Stream a -> [a]
 {-# INLINE [0] unstreamList #-}
 
 -- | /O(n)/ Adds a character to the front of a Stream Char.
 cons :: Char -> Stream Char -> Stream Char
-cons w (Stream next0 s0 len) = Stream next (S2 :!: s0) (len+2) -- HINT maybe too high
+cons w (Stream next0 s0 len) = Stream next (S2 :!: s0) (len+1)
     where
       {-# INLINE next #-}
       next (S2 :!: s) = Yield w (S1 :!: s)
 
 -- | /O(n)/ Adds a character to the end of a stream.
 snoc :: Stream Char -> Char -> Stream Char
-snoc (Stream next0 xs0 len) w = Stream next (J xs0) (len+2) -- HINT maybe too high
+snoc (Stream next0 xs0 len) w = Stream next (J xs0) (len+1)
   where
     {-# INLINE next #-}
     next (J xs) = case next0 xs of
 uncons (Stream next s0 len) = loop_uncons s0
     where
       loop_uncons !s = case next s of
-                         Yield x s1 -> Just (x, Stream next s1 (len-1)) -- HINT maybe too high
+                         Yield x s1 -> Just (x, Stream next s1 (len-1))
                          Skip s'    -> loop_uncons s'
                          Done       -> Nothing
 {-# INLINE [0] uncons #-}
 -- | /O(1)/ Returns all characters after the head of a Stream Char, which must
 -- be non-empty.
 tail :: Stream Char -> Stream Char
-tail (Stream next0 s0 len) = Stream next (False :!: s0) (len-1) -- HINT maybe too high
+tail (Stream next0 s0 len) = Stream next (False :!: s0) (len-1)
     where
       {-# INLINE next #-}
       next (False :!: s) = case next0 s of
 -- | /O(1)/ Returns all but the last character of a Stream Char, which
 -- must be non-empty.
 init :: Stream Char -> Stream Char
-init (Stream next0 s0 len) = Stream next (N :!: s0) (len-1) -- HINT maybe too high
+init (Stream next0 s0 len) = Stream next (N :!: s0) (len-1)
     where
       {-# INLINE next #-}
       next (N :!: s) = case next0 s of
 -- ----------------------------------------------------------------------------
 -- * Stream transformations
 
--- | /O(n)/ 'map' @f @xs is the Stream Char obtained by applying @f@ to each element of
--- @xs@.
+-- | /O(n)/ 'map' @f @xs is the Stream Char obtained by applying @f@
+-- to each element of @xs@.
 map :: (Char -> Char) -> Stream Char -> Stream Char
-map f (Stream next0 s0 len) = Stream next s0 len -- HINT depends on f
+map f (Stream next0 s0 len) = Stream next s0 len
     where
       {-# INLINE next #-}
       next !s = case next0 s of
 -- | /O(n)/ Take a character and place it between each of the
 -- characters of a 'Stream Char'.
 intersperse :: Char -> Stream Char -> Stream Char
-intersperse c (Stream next0 s0 len) = Stream next (s0 :!: N :!: S1) len -- HINT maybe too low
+intersperse c (Stream next0 s0 len) = Stream next (s0 :!: N :!: S1) len
     where
       {-# INLINE next #-}
       next (s :!: N :!: S1) = case next0 s of
 {-# INLINE [0] toLower #-}
 
 justifyLeftI :: Integral a => a -> Char -> Stream Char -> Stream Char
-justifyLeftI k c (Stream next0 s0 len) = Stream next (s0 :!: S1 :!: 0) newLen
+justifyLeftI k c (Stream next0 s0 len) =
+    Stream next (s0 :!: S1 :!: 0) (larger (fromIntegral k) len)
   where
-    j = fromIntegral k
-    newLen | j > len   = j
-           | otherwise = len
     next (s :!: S1 :!: n) =
         case next0 s of
           Done       -> next (s :!: S2 :!: n)
 
 replicateI :: Int64 -> Stream Char -> Stream Char
 replicateI n (Stream next0 s0 len) =
-    Stream next (0 :!: s0) (max 0 (fromIntegral n * len))
+    Stream next (0 :!: s0) (fromIntegral (max 0 n) * len)
   where
     next (k :!: s)
         | k >= n = Done
 -- length of the stream.
 take :: Integral a => a -> Stream Char -> Stream Char
 take n0 (Stream next0 s0 len) =
-    Stream next (n0 :!: s0) (max 0 (len - fromIntegral n0)) -- HINT maybe too high
+    Stream next (n0 :!: s0) (smaller len (fromIntegral (max 0 n0)))
     where
       {-# INLINE next #-}
       next (n :!: s) | n <= 0    = Done
 -- length of the stream.
 drop :: Integral a => a -> Stream Char -> Stream Char
 drop n0 (Stream next0 s0 len) =
-    Stream next (J (max 0 n0) :!: s0) (len - fromIntegral n0) -- HINT maybe too high
+    Stream next (J n0 :!: s0) (len - fromIntegral (max 0 n0))
   where
     {-# INLINE next #-}
     next (J n :!: s)
-      | n == 0    = Skip (N :!: s)
+      | n <= 0    = Skip (N :!: s)
       | otherwise = case next0 s of
           Done       -> Done
           Skip    s' -> Skip (J n    :!: s')
 -- | zipWith generalises 'zip' by zipping with the function given as
 -- the first argument, instead of a tupling function.
 zipWith :: (a -> a -> b) -> Stream a -> Stream a -> Stream b
-zipWith f (Stream next0 sa0 len1) (Stream next1 sb0 len2) = Stream next (sa0 :!: sb0 :!: N) (min len1 len2)
+zipWith f (Stream next0 sa0 len1) (Stream next1 sb0 len2) =
+    Stream next (sa0 :!: sb0 :!: N) (smaller len1 len2)
     where
       {-# INLINE next #-}
       next (sa :!: sb :!: N) = case next0 sa of

File Data/Text/Fusion/Internal.hs

     , empty
     ) where
 
+import Data.Text.Fusion.Size
 import Data.Word (Word8)
 
 -- | Specialised, strict Maybe-like type.
     forall s. Stream
     (s -> Step s a)             -- stepper function
     !s                          -- current state
-    {-# UNPACK #-}!Int          -- length hint
+    {-# UNPACK #-} !Size        -- size hint
 
 -- | /O(n)/ Determines if two streams are equal.
 eq :: (Eq a) => Stream a -> Stream a -> Bool

File Data/Text/Fusion/Size.hs

+{-# OPTIONS_GHC -fno-warn-missing-methods #-}
+-- |
+-- Module      : Data.Text.Fusion.Internal
+-- Copyright   : (c) Roman Leshchinskiy 2008,
+--               (c) Bryan O'Sullivan 2009
+--
+-- License     : BSD-style
+-- Maintainer  : bos@serpentine.com, rtharper@aftereternity.co.uk,
+--               duncan@haskell.org
+-- Stability   : experimental
+-- Portability : portable
+--
+-- Size hints.
+
+module Data.Text.Fusion.Size
+    (
+      Size
+    , exactSize
+    , maxSize
+    , unknownSize
+    , smaller
+    , larger
+    , toMax
+    , upperBound
+    , lowerBound
+    , isEmpty
+    ) where
+
+import Control.Exception (assert)
+
+data Size = Exact {-# UNPACK #-} !Int -- ^ Exact size.
+          | Max   {-# UNPACK #-} !Int -- ^ Upper bound on size.
+          | Unknown                   -- ^ Unknown size.
+            deriving (Eq, Show)
+
+exactSize :: Int -> Size
+exactSize n = assert (n >= 0) Exact n
+{-# INLINE exactSize #-}
+
+maxSize :: Int -> Size
+maxSize n = assert (n >= 0) Max n
+{-# INLINE maxSize #-}
+
+unknownSize :: Size
+unknownSize = Unknown
+{-# INLINE unknownSize #-}
+
+instance Num Size where
+    (+) = addSize
+    (-) = subtractSize
+    (*) = mulSize
+
+    fromInteger = f where f = Exact . fromInteger
+                          {-# INLINE f #-}
+
+addSize :: Size -> Size -> Size
+addSize (Exact m) (Exact n) = Exact (m+n)
+addSize (Exact m) (Max   n) = Max   (m+n)
+addSize (Max   m) (Exact n) = Max   (m+n)
+addSize (Max   m) (Max   n) = Max   (m+n)
+addSize _          _       = Unknown
+{-# INLINE addSize #-}
+
+subtractSize :: Size -> Size -> Size
+subtractSize   (Exact m) (Exact n) = Exact (max (m-n) 0)
+subtractSize   (Exact m) (Max   _) = Max   m
+subtractSize   (Max   m) (Exact n) = Max   (max (m-n) 0)
+subtractSize a@(Max   _) (Max   _) = a
+subtractSize a@(Max   _) Unknown   = a
+subtractSize _         _           = Unknown
+{-# INLINE subtractSize #-}
+
+mulSize :: Size -> Size -> Size
+mulSize (Exact m) (Exact n) = Exact (m*n)
+mulSize (Exact m) (Max   n) = Max   (m*n)
+mulSize (Max   m) (Exact n) = Max   (m*n)
+mulSize (Max   m) (Max   n) = Max   (m*n)
+mulSize _          _       = Unknown
+{-# INLINE mulSize #-}
+
+-- | Minimum of two size hints.
+smaller :: Size -> Size -> Size
+smaller   (Exact m) (Exact n) = Exact (m `min` n)
+smaller   (Exact m) (Max   n) = Max   (m `min` n)
+smaller   (Exact m) Unknown   = Max   m
+smaller   (Max   m) (Exact n) = Max   (m `min` n)
+smaller   (Max   m) (Max   n) = Max   (m `min` n)
+smaller a@(Max   _) Unknown   = a
+smaller   Unknown   (Exact n) = Max   n
+smaller   Unknown   (Max   n) = Max   n
+smaller   Unknown   Unknown   = Unknown
+{-# INLINE smaller #-}
+
+-- | Maximum of two size hints.
+larger :: Size -> Size -> Size
+larger   (Exact m)   (Exact n)             = Exact (m `max` n)
+larger a@(Exact m) b@(Max   n) | m >= n    = a
+                               | otherwise = b
+larger a@(Max   m) b@(Exact n) | n >= m    = b
+                               | otherwise = a
+larger   (Max   m)   (Max   n)             = Max   (m `max` n)
+larger _             _                     = Unknown
+{-# INLINE larger #-}
+
+-- | Convert a size hint to an upper bound.
+toMax :: Size -> Size
+toMax   (Exact n) = Max n
+toMax a@(Max   _) = a
+toMax   Unknown   = Unknown
+{-# INLINE toMax #-}
+
+-- | Compute the minimum size from a size hint.
+lowerBound :: Size -> Int
+lowerBound (Exact n) = n
+lowerBound _         = 0
+{-# INLINE lowerBound #-}
+
+-- | Compute the maximum size from a size hint, if possible.
+upperBound :: Int -> Size -> Int
+upperBound _ (Exact n) = n
+upperBound _ (Max   n) = n
+upperBound k _         = k
+{-# INLINE upperBound #-}
+
+isEmpty :: Size -> Bool
+isEmpty (Exact n) = n <= 0
+isEmpty (Max   n) = n <= 0
+isEmpty _         = False
+{-# INLINE isEmpty #-}

File Data/Text/Lazy/Encoding/Fusion.hs

 import Data.Text.Encoding.Error
 import Data.Text.Fusion (Step(..), Stream(..))
 import Data.Text.Fusion.Internal (M(..), PairS(..), S(..))
+import Data.Text.Fusion.Size
 import Data.Text.UnsafeChar (unsafeChr8)
 import Data.Word (Word8)
 import qualified Data.Text.Encoding.Utf8 as U8
 import Control.Exception (assert)
 import qualified Data.ByteString.Internal as B
 
-unknownLength :: Int
-unknownLength = 4
-
 -- | /O(n)/ Convert a lazy 'ByteString' into a 'Stream Char', using
 -- UTF-8 encoding.
 streamUtf8 :: OnDecodeError -> ByteString -> Stream Char
-streamUtf8 onErr bs0 = Stream next (bs0 :!: empty :!: 0) unknownLength
+streamUtf8 onErr bs0 = Stream next (bs0 :!: empty :!: 0) unknownSize
     where
       empty = S N N N N
       {-# INLINE next #-}
 
 -- | /O(n)/ Convert a 'Stream' 'Word8' to a lazy 'ByteString'.
 unstreamChunks :: Int -> Stream Word8 -> ByteString
-unstreamChunks chunkSize (Stream next s0 len0) = chunk s0 len0
+unstreamChunks chunkSize (Stream next s0 len0) = chunk s0 (upperBound 4 len0)
   where chunk s1 len1 = unsafePerformIO $ do
-          let len = min (max len1 unknownLength) chunkSize
+          let len = min len1 chunkSize
           mallocByteString len >>= loop len 0 s1
           where
             loop !n !off !s fp = case next s of

File Data/Text/Lazy/Fusion.hs

 import Prelude hiding (length)
 import qualified Data.Text.Fusion.Common as S
 import Data.Text.Fusion.Internal
+import Data.Text.Fusion.Size (isEmpty)
 import Data.Text.Lazy.Internal
 import qualified Data.Text.Internal as I
 import qualified Data.Text.Array as A
 -- chunk size.
 unstreamChunks :: Int -> Stream Char -> Text
 unstreamChunks chunkSize (Stream next s0 len0)
-  | len0 == 0 = Empty
-  | otherwise = outer s0
+  | isEmpty len0 = Empty
+  | otherwise    = outer s0
   where
     outer s = case next s of
                 Done       -> Empty

File tests/Makefile

 ghc-opt-flags = -O0
 ghc-base-flags := -funbox-strict-fields -package criterion \
 	-package bytestring -package QuickCheck -package test-framework \
-	-package test-framework-quickcheck -ignore-package text
+	-package test-framework-quickcheck -ignore-package text \
+	-fno-ignore-asserts
 ghc-base-flags += -Wall -fno-warn-orphans -fno-warn-missing-signatures
 ghc-flags := $(ghc-base-flags) -i../dist/build -package-name text-$(version)
 ghc-hpc-flags := $(ghc-base-flags) -fhpc -fno-ignore-asserts -odir hpcdir \

File tests/Properties.hs

 import Control.Exception (SomeException, try)
 import qualified Data.Text.Fusion as S
 import qualified Data.Text.Fusion.Common as S
+import Data.Text.Fusion.Size
 import qualified Data.Text.Lazy.Encoding as EL
 import qualified Data.Text.Lazy.Fusion as SL
 import qualified Data.Text.UnsafeShift as U
 import Test.Framework.Providers.QuickCheck (testProperty)
 import Data.Text.Search
 
-import QuickCheckUtils (NotEmpty(..))
+import QuickCheckUtils (NotEmpty(..), small)
 
 -- Ensure that two potentially bottom values (in the sense of crashing
 -- for some inputs, not looping infinitely) either both crash, or both
 -- For tests that have O(n^2) running times or input sizes, resize
 -- their inputs to the square root of the originals.
 unsquare :: (Arbitrary a, Show a, Testable b) => (a -> b) -> Property
-unsquare = forAll . sized $ \n -> resize (smaller n) arbitrary
-    where smaller = round . (sqrt :: Double -> Double) . fromIntegral
+unsquare = forAll . sized $ \n -> resize (smallish n) arbitrary
+    where smallish = round . (sqrt :: Double -> Double) . fromIntegral
 
 s_Eq s            = (s==)    `eq` ((S.streamList s==) . S.streamList)
     where _types = s :: String
 tl_IsString       = fromString  `eqP` (TL.unpack . fromString)
 
 s_cons x          = (x:)     `eqP` (unpackS . S.cons x)
+s_cons_s x        = (x:)     `eqP` (unpackS . S.unstream . S.cons x)
 sf_cons p x       = ((x:) . L.filter p) `eqP` (unpackS . S.cons x . S.filter p)
 t_cons x          = (x:)     `eqP` (unpackS . T.cons x)
 tl_cons x         = (x:)     `eqP` (unpackS . TL.cons x)
 unpack2 = unpackS *** unpackS
 
 s_take n          = L.take n      `eqP` (unpackS . S.take n)
+s_take_s m        = L.take n      `eqP` (unpackS . S.unstream . S.take n)
+  where n = small m
 sf_take p n       = (L.take n . L.filter p) `eqP` (unpackS . S.take n . S.filter p)
 t_take n          = L.take n      `eqP` (unpackS . T.take n)
 tl_take n         = L.take n      `eqP` (unpackS . TL.take (fromIntegral n))
 s_drop n          = L.drop n      `eqP` (unpackS . S.drop n)
+s_drop_s m        = L.drop n      `eqP` (unpackS . S.unstream . S.drop n)
+  where n = small m
 sf_drop p n       = (L.drop n . L.filter p) `eqP` (unpackS . S.drop n . S.filter p)
 t_drop n          = L.drop n      `eqP` (unpackS . T.drop n)
 tl_drop n         = L.drop n      `eqP` (unpackS . TL.drop (fromIntegral n))
-s_take_drop n     = (L.take n . L.drop n) `eqP` (unpackS . S.take n . S.drop n)
+s_take_drop m     = (L.take n . L.drop n) `eqP` (unpackS . S.take n . S.drop n)
+  where n = small m
+s_take_drop_s m   = (L.take n . L.drop n) `eqP` (unpackS . S.unstream . S.take n . S.drop n)
+  where n = small m
 s_takeWhile p     = L.takeWhile p `eqP` (unpackS . S.takeWhile p)
 sf_takeWhile q p  = (L.takeWhile p . L.filter q) `eqP` (unpackS . S.takeWhile p . S.filter q)
 t_takeWhile p     = L.takeWhile p `eqP` (unpackS . T.takeWhile p)
 -- themselves.
 shorten :: Int -> S.Stream a -> S.Stream a
 shorten n t@(S.Stream arr off len)
-    | n < len && n > 0 = S.Stream arr off n
-    | otherwise        = t
+    | n > 0     = S.Stream arr off (smaller (exactSize n) len) 
+    | otherwise = t
 
 main = defaultMain tests
 
 
   testGroup "basics" [
     testProperty "s_cons" s_cons,
+    testProperty "s_cons_s" s_cons_s,
     testProperty "sf_cons" sf_cons,
     testProperty "t_cons" t_cons,
     testProperty "tl_cons" tl_cons,
   testGroup "substrings" [
     testGroup "breaking" [
       testProperty "s_take" s_take,
+      testProperty "s_take_s" s_take_s,
       testProperty "sf_take" sf_take,
       testProperty "t_take" t_take,
       testProperty "tl_take" tl_take,
       testProperty "s_drop" s_drop,
+      testProperty "s_drop_s" s_drop_s,
       testProperty "sf_drop" sf_drop,
       testProperty "t_drop" t_drop,
       testProperty "tl_drop" tl_drop,
       testProperty "s_take_drop" s_take_drop,
+      testProperty "s_take_drop_s" s_take_drop_s,
       testProperty "s_takeWhile" s_takeWhile,
       testProperty "sf_takeWhile" sf_takeWhile,
       testProperty "t_takeWhile" t_takeWhile,
     Data.Text.Fusion.CaseMapping
     Data.Text.Fusion.Common
     Data.Text.Fusion.Internal
+    Data.Text.Fusion.Size
     Data.Text.Internal
     Data.Text.Lazy.Encoding.Fusion
     Data.Text.Lazy.Fusion