packages feed

packed (empty) → 0.1.0

raw patch · 13 files changed

+2006/−0 lines, 13 filesdep +basedep +containersdep +gaugesetup-changed

Dependencies added: base, containers, gauge, ghc-prim, hedgehog, packed, primitive, tasty, tasty-hedgehog, tasty-hunit

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for bytearray++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Andrew Martin (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Andrew Martin nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,1 @@+# bytearray
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bench/Main.hs view
@@ -0,0 +1,98 @@+{-# OPTIONS_GHC -O2 -Wall #-}++import Gauge (bgroup,bench,whnf)+import Gauge.Main (defaultMain)+import Packed.Bytes.Small (ByteArray)+import Packed.Text (Text)+import Packed.Bytes (Bytes)++import qualified Packed.Bytes.Small as BA+import qualified Packed.Text as T+import qualified GHC.OldList as L++main :: IO ()+main = do+  defaultMain+    [ bgroup "ByteArray"+      [ bench "findByte" $ whnf (BA.findByte 0x80) byteArrayA+      , bench "zipAnd" $ whnf (BA.zipAnd byteArrayA) byteArrayB+      ]+    , bgroup "Text"+      [ bgroup "toUpper"+        -- Not totally fair. The number 5000 refers to the character count,+        -- meaning that asian scripts end up with more bytes.+        [ bench "ascii" $ whnf T.toUpper textAscii5000+        , bench "latin" $ whnf T.toUpper textLatin5000+        , bench "asian" $ whnf T.toUpper textAsian5000+        ]+      , bgroup "decodeUtf8"+        [ bench "ascii" $ whnf T.decodeUtf8 encodedAscii5000+        , bench "latin" $ whnf T.decodeUtf8 encodedLatin5000+        , bench "asian" $ whnf T.decodeUtf8 encodedAsian5000+        ]+      ]+    ]++byteArrayA :: ByteArray+byteArrayA = BA.pack $ L.concat+  [ L.take 5000 (L.cycle (enumFromTo 0x00 0x7F))+  , [0x80]+  , L.take 2000 (L.cycle (enumFromTo 0xB0 0xFF))+  ]++byteArrayB :: ByteArray+byteArrayB = BA.pack $ L.concat+  [ L.take 2000 (L.cycle (enumFromTo 0x80 0xFF))+  , [0x70]+  , L.take 3000 (L.cycle (enumFromTo 0x20 0x60))+  ]++encodedAscii5000 :: Bytes+encodedAscii5000 = T.encodeUtf8 textAscii5000++encodedLatin5000 :: Bytes+encodedLatin5000 = T.encodeUtf8 textLatin5000++encodedAsian5000 :: Bytes+encodedAsian5000 = T.encodeUtf8 textAsian5000++textAscii5000 :: Text+textAscii5000 = T.pack $ L.concat+  [ L.take 2000 (L.cycle (enumFromTo 'a' 'm' ++ [','] ++ enumFromTo 'A' 'H' ++ ['.']))+  , L.take 3000 (L.cycle "The Old Tavern Across The Town")+  ]++textLatin5000 :: Text+textLatin5000 = T.pack $ L.take 5000 $ L.cycle $ L.concat+  [ "Suzanne et Joseph étaient nés dans les deux premières années de leur. "+  , "arrivée à la colonie. Après la naissance de Suzanne, la mère abandonna "+  , "l'enseignement d'état. "+  , "Mentiría si dijera que era del todo nuevo el sentimiento de que ya no "+  , "iba a poder ser más que lo que era, que era un hombre que había "+  , "envejecido más de lo que suponía, que había sospechado tener toda "+  , "la vida por delante y había ido dejando pasar los años a la espera "+  , "de que llegara su momento, y ahora la tenía a su espalda. "+  ]++textAsian5000 :: Text+textAsian5000 = T.pack $ L.take 5000 $ L.cycle $ L.concat+  [ "유구한 역사와 전통에 빛나는 우리 대한 국민은 3·1 운동으로 건립된 "+  , "대한민국 임시 정부의 법통과 불의에 항거한 4·19 민주 이념을 계승하고, "+  , "조국의 민주 개혁과 평화적 통일의 사명에 입각하여 정의·인도와 동포애로써 "+  , "민족의 단결을 공고히 하고, 모든 사회적 폐습과 불의를 타파하며, 자율과 "+  , "조화를 바탕으로 자유 민주적 기본 질서를 더욱 확고히 하여 정치·경제·사회·문화의 "+  , "모든 영역에 있어서 각인의 기회를 균등히 하고, 능력을 최고도로 발휘하게 하며, "+  , "자유와 권리에 따르는 책임과 의무를 완수하게 하여, 안으로는 국민 생활의 균등한 "+  , "향상을 기하고 밖으로는 항구적인 세계 평화와 인류 공영에 이바지함으로써 우리들과 "+  , "우리들의 자손의 안전과 자유와 행복을 영원히 확보할 것을 다짐하면서 1948년 7월 12일에 "+  , "제정되고 8차에 걸쳐 개정된 헌법을 이제 국회의 의결을 거쳐 국민 투표에 의하여 개정한다."+  ]++-- textFrench :: Text+-- textFrench = T.pack $ L.concat+--   [ "Suzanne et Joseph étaient nés dans les deux premières années de leur. "+--   , "arrivée à la colonie. Après la naissance de Suzanne, la mère abandonna "+--   , "l'enseignement d'état. "+--   ]+  +
+ packed.cabal view
@@ -0,0 +1,61 @@+name: packed+version: 0.1.0+description: Please see the README on Github at <https://github.com/andrewthad/bytearray#readme>+homepage: https://github.com/andrewthad/bytearray#readme+bug-reports: https://github.com/andrewthad/bytearray/issues+author: Andrew Martin+maintainer: andrew.thaddeus@gmail.com+copyright: 2017 Andrew Martin+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+  ChangeLog.md+  README.md++source-repository head+  type: git+  location: https://github.com/andrewthad/bytearray++library+  hs-source-dirs: src+  build-depends:+      base >=4.10 && <5+    , ghc-prim >= 0.5.1.1 && < 0.6+    , primitive >= 0.6 && < 0.7+  exposed-modules:+    Packed.Bytes+    Packed.Bytes.Window+    Packed.Bytes.Mutable+    Packed.Bytes.Small+    Packed.Text+    Packed.Text.Small+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+      test+  build-depends:+      base >=4.7 && <5+    , packed+    , hedgehog >= 0.5+    , tasty-hedgehog >= 0.1.0.2+    , tasty-hunit >= 0.9+    , tasty >= 1.0+    , containers >= 0.5.10+    , ghc-prim+  default-language: Haskell2010++benchmark bench+  type: exitcode-stdio-1.0+  build-depends:+      base+    , packed+    , gauge+  default-language: Haskell2010+  hs-source-dirs: bench+  main-is: Main.hs
+ src/Packed/Bytes.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE BangPatterns #-}++{-# OPTIONS_GHC+ -Weverything+ -fno-warn-unsafe+ -fno-warn-implicit-prelude+ -fno-warn-missing-import-lists+ -O2+#-}++module Packed.Bytes+  ( Bytes(..)+  , pack+  , unpack+  , drop+  , dropEnd+  , replicate+  , length+  , foldl'+  , take+  , empty+  , findByte+    -- * Characters+  , isAscii+  ) where++import Prelude hiding (take,length,replicate,drop)++import Packed.Bytes.Small (ByteArray(..))+import Data.Word (Word8)+import qualified Packed.Bytes.Window as BAW+import qualified Packed.Bytes.Small as BA++data Bytes = Bytes+  {-# UNPACK #-} !ByteArray -- payload+  {-# UNPACK #-} !Int -- offset+  {-# UNPACK #-} !Int -- length++instance Eq Bytes where+  Bytes arrA offA lenA == Bytes arrB offB lenB =+    if lenA == lenB+      then BAW.equality offA offB lenA arrA arrB+      else False++pack :: [Word8] -> Bytes+pack bs = let arr = BA.pack bs in Bytes arr 0 (BA.length arr)++unpack :: Bytes -> [Word8]+unpack (Bytes arr off len) = go off+  where+  go :: Int -> [Word8]+  go !ix = if ix < len + off+    then BA.unsafeIndex arr ix : go (ix + 1)+    else []++drop :: Int -> Bytes -> Bytes+drop n (Bytes arr off len) = if len > n+  then Bytes arr (off + n) (len - n)+  else empty++dropEnd :: Int -> Bytes -> Bytes+dropEnd n (Bytes arr off len) = if len > n+  then Bytes arr off (len - n)+  else empty++replicate :: Int -> Word8 -> Bytes+replicate len w = fromByteArray (BA.replicate len w)++fromByteArray :: ByteArray -> Bytes+fromByteArray ba = Bytes ba 0 (BA.length ba)++length :: Bytes -> Int+length (Bytes _ _ len) = len++findByte :: Word8 -> Bytes -> Maybe Int+findByte !w (Bytes arr off len) = case BAW.findByte off len w arr of+  Just ix -> Just (ix - off)+  Nothing -> Nothing++foldl' :: (a -> Word8 -> a) -> a -> Bytes -> a+foldl' f !acc0 (Bytes arr off len) = BAW.foldl' off len f acc0 arr++take :: Int -> Bytes -> Bytes+take !n (Bytes arr off len) = if n < len+  then Bytes arr off (len - n)+  else empty++empty :: Bytes+empty = Bytes BA.empty 0 0++isAscii :: Bytes -> Bool+isAscii (Bytes arr off len) = BAW.isAscii off len arr++-- In this implementation, we overallocate on each side to+-- make things line up with machine word boundaries. This+-- make the reversal eight times faster.+-- reverse :: Bytes -> Bytes+-- reverse (Bytes arr off len) =+--   let !leftWordIx = quot off (PM.sizeOf (undefined :: Word))+--       !rightWordIx = quot (off + len) (PM.sizeOf (undefined :: Word))+
+ src/Packed/Bytes/Mutable.hs view
@@ -0,0 +1,1 @@+module Packed.Bytes.Mutable where
+ src/Packed/Bytes/Small.hs view
@@ -0,0 +1,184 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE ScopedTypeVariables #-}++{-# OPTIONS_GHC+ -Weverything+ -fno-warn-unsafe+ -fno-warn-implicit-prelude+ -fno-warn-missing-import-lists+ -O2+#-}++module Packed.Bytes.Small+  ( ByteArray(..)+  , empty+  , pack+  , unpack+  , singleton+  , append+  , replicate+  , length+  , findByte+  , foldl'+  , take+  , cons+  , uncons+  , reverse+    -- * Zip+  , zipAnd+  , zipOr+  , zipXor+    -- * Characters+  , isAscii+    -- * Unsafe+  , unsafeIndex+  ) where++import Prelude hiding (replicate,length,take,reverse)++import Control.Monad.Primitive (primitive_)+import Control.Monad.ST (runST,ST)+import Data.Primitive.ByteArray (ByteArray(..),MutableByteArray(..))+import GHC.Exts (setByteArray#,word2Int#,byteSwap#)+import GHC.Int (Int(I#))+import GHC.Word (Word8(W8#),Word(W#))+import qualified GHC.OldList as L+import qualified Packed.Bytes.Window as BAW+import qualified Data.Primitive as PM++singleton :: Word8 -> ByteArray+singleton w = runST $ do+  marr <- PM.newByteArray 1+  PM.writeByteArray marr 0 w+  PM.unsafeFreezeByteArray marr++pack :: [Word8] -> ByteArray+pack ws0 = runST $ do+  marr <- PM.newByteArray (L.length ws0)+  let go [] !_ = return ()+      go (w : ws) !ix = writeByteArrayWord8 marr ix w >> go ws (ix + 1)+  go ws0 0+  PM.unsafeFreezeByteArray marr++unpack :: ByteArray -> [Word8]+unpack arr = go 0 where+  go :: Int -> [Word8]+  go !ix = if ix < length arr+    then unsafeIndex arr ix : go (ix + 1)+    else []++append :: ByteArray -> ByteArray -> ByteArray+append !a !b = do+  let !lenA = length a+      !lenB = length b+      !len = lenA + lenB+   in runST $ do+        !marr <- PM.newByteArray len+        PM.copyByteArray marr 0 a 0 lenA+        PM.copyByteArray marr lenA b 0 lenB+        PM.unsafeFreezeByteArray marr++cons :: Word8 -> ByteArray -> ByteArray+cons w arr = runST $ do+  let !lenArr = length arr+  marr <- PM.newByteArray (lenArr + 1)+  PM.writeByteArray marr 0 w+  PM.copyByteArray marr 1 arr 0 lenArr+  PM.unsafeFreezeByteArray marr++uncons :: ByteArray -> Maybe (Word8, ByteArray)+uncons arr0 = if lenArr > 0+  then+    let !arr1 = runST $ do+          marr <- PM.newByteArray (lenArr - 1)+          PM.copyByteArray marr 0 arr0 1 (lenArr - 1)+          PM.unsafeFreezeByteArray marr+        w :: Word8+        !w = PM.indexByteArray arr0 0+     in Just (w,arr1)+  else Nothing+  where+  !lenArr = length arr0++replicate :: Int -> Word8 -> ByteArray+replicate len@(I# len#) (W8# w#) = runST $ do+  marr@(MutableByteArray marr#) <- PM.newByteArray len+  primitive_ (setByteArray# marr# 0# len# (word2Int# w#))+  PM.unsafeFreezeByteArray marr++length :: ByteArray -> Int+length = PM.sizeofByteArray++findByte :: Word8 -> ByteArray -> Maybe Int+findByte !w !arr = BAW.findByte 0 (length arr) w arr++foldl' :: (a -> Word8 -> a) -> a -> ByteArray -> a+foldl' f !acc0 !arr = BAW.foldl' 0 (length arr) f acc0 arr++take :: Int -> ByteArray -> ByteArray+take !n !arr = if n < length arr+  then runST $ do+    marr <- PM.newByteArray n+    PM.copyByteArray marr 0 arr 0 n+    PM.unsafeFreezeByteArray marr+  else empty++empty :: ByteArray+empty = runST (PM.newByteArray 0 >>= PM.unsafeFreezeByteArray)++-- | Reverse the bytes in a byte array. This operation is eight+--   times faster if the length of the byte array divides eight+--   evenly.+reverse :: ByteArray -> ByteArray+reverse !arr = runST $ do+  marr <- PM.newByteArray (length arr)+  let !(!quotient,!remainder) = quotRem (length arr) (PM.sizeOf (undefined :: Word))+  if remainder == 0+    then goFast 0 quotient marr+    else goSlow 0 (length arr) marr+  PM.unsafeFreezeByteArray marr+  where+  goFast :: forall s. Int -> Int -> MutableByteArray s -> ST s ()+  goFast !ix !len !marr = if ix < len+    then do+      let !w = unsafeIndexWord arr ix+      writeByteArrayWord marr (len - ix) (byteSwap w)+      goFast (ix + 1) len marr+    else return ()+  goSlow :: forall s. Int -> Int -> MutableByteArray s -> ST s ()+  goSlow !ix !len !marr = if ix < len+    then do+      let !w = unsafeIndex arr ix+      writeByteArrayWord8 marr (len - ix) w+      goSlow (ix + 1) len marr+    else return ()++byteSwap :: Word -> Word+byteSwap (W# w) = W# (byteSwap# w)++writeByteArrayWord :: MutableByteArray s -> Int -> Word -> ST s ()+writeByteArrayWord = PM.writeByteArray++writeByteArrayWord8 :: MutableByteArray s -> Int -> Word8 -> ST s ()+writeByteArrayWord8 = PM.writeByteArray++-- this is only used internally+unsafeIndexWord :: ByteArray -> Int -> Word+unsafeIndexWord = PM.indexByteArray++-- | Does not check to see if the index is in bounds.+unsafeIndex :: ByteArray -> Int -> Word8+unsafeIndex = PM.indexByteArray++zipAnd :: ByteArray -> ByteArray -> ByteArray+zipAnd x y = BAW.zipAnd 0 (length x) 0 (length y) x y++zipOr :: ByteArray -> ByteArray -> ByteArray+zipOr x y = BAW.zipOr 0 (length x) 0 (length y) x y++zipXor :: ByteArray -> ByteArray -> ByteArray+zipXor x y = BAW.zipXor 0 (length x) 0 (length y) x y++isAscii :: ByteArray -> Bool+isAscii x = BAW.isAscii 0 (length x) x
+ src/Packed/Bytes/Window.hs view
@@ -0,0 +1,517 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BinaryLiterals #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++{-# OPTIONS_GHC+ -Weverything+ -fno-warn-unsafe+ -fno-warn-implicit-prelude+ -O2+#-}++module Packed.Bytes.Window+  ( findByte+  , foldl'+  , reverse+  , zipAnd+  , zipOr+  , zipXor+  , equality+    -- * Characters+  , isAscii+  , isUtf8+  , findNonAscii'+  ) where++import Data.Primitive (ByteArray(ByteArray))+import Data.Word (Word8)+import GHC.Types (RuntimeRep,TYPE)+import GHC.Int (Int(I#))+import GHC.Word (Word8(W8#),Word(W#))+import GHC.Exts (Int#,Word#,ByteArray#)+import Data.Bits (xor,(.|.),(.&.),complement,unsafeShiftL)+import Control.Monad.ST (ST,runST)+import qualified Data.Primitive as PM++type Maybe# (a :: TYPE (r :: RuntimeRep)) = (# (# #) | a #)++boxMaybeInt :: Maybe# Int# -> Maybe Int+boxMaybeInt = \case+  (# | a #) -> Just (I# a)+  (# (# #) | #) -> Nothing++unboxInt :: Int -> Int#+unboxInt (I# i) = i++unboxWord :: Word -> Word#+unboxWord (W# i) = i++-- | Finds the first occurrence of the given byte.+{-# INLINE findByte #-}+findByte :: Int -> Int -> Word8 -> ByteArray -> Maybe Int+findByte (I# off) (I# len) (W8# w) (ByteArray arr) =+  boxMaybeInt (findByte' off len w arr)++{-# NOINLINE findByte' #-}+findByte' :: Int# -> Int# -> Word# -> ByteArray# -> Maybe# Int#+findByte' !off# !len0# !w0# !arr0# = +  let !off = I# off#+      !len0 = I# len0#+      !end0 = off + len0+      !beginMachWord = alignUp off+      !endMachWord = alignDown end0+   in if len0 < PM.sizeOf (undefined :: Word)+        then go off end0+        else case go off (beginMachWord * PM.sizeOf (undefined :: Word)) of+          (# | ix #) -> (# | ix #)+          (# (# #) | #) -> case goMachWord beginMachWord endMachWord (broadcastWord8 w) of+            (# | ix #) -> (# | ix #)+            (# (# #) | #) -> case go (endMachWord * PM.sizeOf (undefined :: Word)) end0 of+              (# | ix #) -> (# | ix #)+              (# (# #) | #) -> (# (# #) | #)+  where+  !w = W8# w0#+  !arr = ByteArray arr0#+  go :: Int -> Int -> Maybe# Int#+  go !ix !end = if ix < end+    then if PM.indexByteArray arr ix == w+      then (# | unboxInt ix #)+      else go (ix + 1) end+    else (# (# #) | #)+  -- The start and end index here are given in machine Word elements,+  -- not Word8 elements.+  goMachWord :: Int -> Int -> Word -> Maybe# Int#+  goMachWord !ix !end !artifact = if ix < end+    then case detectArtifact (unsafeIndexWord arr ix) artifact of+      0 -> goMachWord (ix + 1) end artifact+      _ -> go -- this call to go should always return Just+        (ix * PM.sizeOf (undefined :: Word)) +        ((ix + 1) * PM.sizeOf (undefined :: Word))+    else (# (# #) | #)++-- cast a Word8 index to a machine Word index, rounding up+alignUp :: Int -> Int+alignUp i =+  let !(!quotient,!remainder) = quotRem i (PM.sizeOf (undefined :: Word))+   in case remainder of+        0 -> quotient+        _ -> quotient + 1++-- cast a Word8 index to a machine Word index, rounding down+alignDown :: Int -> Int+alignDown i = quot i (PM.sizeOf (undefined :: Word))++broadcastWord8 :: Word8 -> Word+broadcastWord8 !w0 = go 8 (fromIntegral w0) where+  go :: Int -> Word -> Word+  go !n !w = if n < 8 * PM.sizeOf (undefined :: Word)+    then go (twice n) (unsafeShiftL w n .|. w)+    else w++twice :: Int -> Int+twice n = n * 2++-- returns non-zero if a null byte is present in the machine word+detectNull :: Word -> Word+detectNull x = (x - repeatHexZeroOne) .&. complement x .&. repeatHexEightZero++detectArtifact :: Word -> Word -> Word+detectArtifact x artifact = detectNull (applyArtifact x artifact)++applyArtifact :: Word -> Word -> Word+applyArtifact = xor++repeatHexZeroOne :: Word+repeatHexZeroOne = div maxBound 255++repeatHexEightZero :: Word+repeatHexEightZero = 128 * (div maxBound 255 :: Word)++foldl' :: forall a. Int -> Int -> (a -> Word8 -> a) -> a -> ByteArray -> a+foldl' !off !len f !acc0 !arr = go acc0 off where+  go :: a -> Int -> a+  go !acc !ix = if ix < off + len+    then go (f acc (PM.indexByteArray arr ix)) (ix + 1)+    else acc++-- this is only used internally+unsafeIndexWord :: ByteArray -> Int -> Word+unsafeIndexWord = PM.indexByteArray++safeIndexWord :: ByteArray -> Int -> Word+safeIndexWord arr ix = if ix < 0 || ix >= (div (PM.sizeofByteArray arr) (PM.sizeOf (undefined :: Word)))+  then error ("safeIndexWord: " ++ show ix ++ " is out of bounds")+  else PM.indexByteArray arr ix++-- this is only used internally+unsafeIndex :: ByteArray -> Int -> Word8+unsafeIndex = PM.indexByteArray++safeIndex :: ByteArray -> Int -> Word8+safeIndex arr ix = if ix < 0 || ix >= PM.sizeofByteArray arr+  then error ("safeIndex: " ++ show ix ++ " is out of bounds")+  else PM.indexByteArray arr ix++-- TODO: optimize this. We could do a whole Word64 at a+-- time if the bytearray is pinned. Maybe even if it+-- isn't pinned.+-- reverse :: Int -> Int -> ByteArray -> ByteArray+-- reverse off len arr = runST++-- | Check if the given slice of the two byte arrays+--   is equal.+equality :: +     Int -- ^ start x+  -> Int -- ^ start y+  -> Int -- ^ length+  -> ByteArray -- ^ array x+  -> ByteArray -- ^ array y+  -> Bool+equality !ixA !ixB !len !arrA !arrB = go 0+  -- TODO: Replace this with compareByteArrays# once GHC 8.4+  -- or GHC 8.6 is released. This will be simpler and much+  -- faster.+  where+  go :: Int -> Bool+  go !ix = if ix < len+    then if safeIndex arrA (ix + ixA) == safeIndex arrB (ix + ixB)+      then go (ix + 1)+      else False+    else True++{-# INLINE zipVectorizable #-}+zipVectorizable ::+     (Word8 -> Word8 -> Word8)+  -> (Word -> Word -> Word)+  -> Int -- start x+  -> Int -- len x+  -> Int -- start y+  -> Int -- len y+  -> ByteArray -- x+  -> ByteArray -- y+  -> ByteArray -- z+zipVectorizable !combine !combineMach !startX !lenX !startY !lenY !x !y = runST action+  where+  action :: forall s. ST s ByteArray+  action = do+    let !len = min lenX lenY+    marr <- PM.newByteArray len+    let !(!quotStartX,!remStartX) = quotRem startX (PM.sizeOf (undefined :: Word))+        !(!quotStartY,!remStartY) = quotRem startY (PM.sizeOf (undefined :: Word))+        go :: Int -> Int -> ST s ()+        go !ix !end = if ix < end+          then do+            PM.writeByteArray marr ix (combine (unsafeIndex x (startX + ix)) (unsafeIndex y (startY + ix)))+            go (ix + 1) end+          else return ()+        goMach :: Int -> Int -> ST s ()+        goMach !ix !end = if ix < end+          then do+            PM.writeByteArray marr ix (combineMach (unsafeIndexWord x (quotStartX + ix)) (unsafeIndexWord y (quotStartY + ix)))+            goMach (ix + 1) end+          else return ()+    if remStartX .|. remStartY == 0 -- if they are both zero+      then do+        let !lenQuotient = quot len (PM.sizeOf (undefined :: Word))+        goMach 0 lenQuotient+        go (lenQuotient * PM.sizeOf (undefined :: Word)) len+      else go 0 len+    PM.unsafeFreezeByteArray marr++zipAnd :: Int -> Int -> Int -> Int -> ByteArray -> ByteArray -> ByteArray+zipAnd x0 xlen y0 ylen x y = zipVectorizable (.&.) (.&.) x0 xlen y0 ylen x y++zipOr :: Int -> Int -> Int -> Int -> ByteArray -> ByteArray -> ByteArray+zipOr x0 xlen y0 ylen x y = zipVectorizable (.|.) (.|.) x0 xlen y0 ylen x y++zipXor :: Int -> Int -> Int -> Int -> ByteArray -> ByteArray -> ByteArray+zipXor x0 xlen y0 ylen x y = zipVectorizable (.|.) (.|.) x0 xlen y0 ylen x y++-- this can be used to implement all predicates or any predicates+{-# INLINE boolVectorizable #-}+boolVectorizable ::+     Bool+  -> (Bool -> Bool -> Bool)+  -> (Word8 -> Bool)+  -> (Word -> Bool)+  -> Int -- start+  -> Int -- len+  -> ByteArray+  -> Bool+boolVectorizable emptyBool together predicate predicateMach !start !len !arr =+  let !end0 = start + len+      !beginMachWord = alignUp start+      !endMachWord = alignDown end0+   in if len < PM.sizeOf (undefined :: Word)+        then go start end0+        else together+          (go start (beginMachWord * PM.sizeOf (undefined :: Word)))+          (together+            (goMachWord beginMachWord endMachWord)+            (go (endMachWord * PM.sizeOf (undefined :: Word)) end0)+          )+  where+  go :: Int -> Int -> Bool+  go !ix !end = if ix < end+    then together (predicate (PM.indexByteArray arr ix)) (go (ix + 1) end)+    else emptyBool+  -- The start and end index here are given in machine Word elements,+  -- not Word8 elements.+  goMachWord :: Int -> Int -> Bool+  goMachWord !ix !end = if ix < end+    then together (predicateMach (unsafeIndexWord arr ix)) (goMachWord (ix + 1) end)+    else emptyBool++{-# INLINE findVectorizable #-}+findVectorizable ::+     (Word8 -> Bool)+  -> (Word -> Bool)+  -> Int -- start+  -> Int -- len+  -> ByteArray+  -> Maybe# Int#+findVectorizable predicate predicateMach !start !len !arr =+  let !end0 = start + len+      !beginMachWord = alignUp start+      !endMachWord = alignDown end0+   in if len < PM.sizeOf (undefined :: Word)+        then go start end0+        else case go start (beginMachWord * PM.sizeOf (undefined :: Word)) of+          (# | ix #) -> (# | ix #)+          (# (# #) | #) -> case goMachWord beginMachWord endMachWord of+            (# | ix #) -> (# | ix #)+            (# (# #) | #) -> case go (endMachWord * PM.sizeOf (undefined :: Word)) end0 of+              (# | ix #) -> (# | ix #)+              (# (# #) | #) -> (# (# #) | #)+  where+  go :: Int -> Int -> Maybe# Int#+  go !ix !end = if ix < end+    then case predicate (safeIndex arr ix) of+      False -> go (ix + 1) end+      True -> (# | unboxInt ix #)+    else (# (# #) | #)+  -- The start and end index here are given in machine Word elements,+  -- not Word8 elements.+  goMachWord :: Int -> Int -> Maybe# Int#+  goMachWord !ix !end = if ix < end+    then case predicateMach (safeIndexWord arr ix) of+      False -> goMachWord (ix + 1) end+      True -> go+        (ix * PM.sizeOf (undefined :: Word)) +        ((ix + 1) * PM.sizeOf (undefined :: Word))+    else (# (# #) | #)++asciiMask :: Word8+asciiMask = 0x80++asciiMachMask :: Word+asciiMachMask = repeatHexEightZero++isAscii :: Int -> Int -> ByteArray -> Bool+isAscii start len arr = boolVectorizable True (&&)+  (\w -> w .&. asciiMask == 0)+  (\w -> w .&. asciiMachMask == 0)+  start len arr++-- This could be exported but it does not seem generally useful.+findNonAscii' :: Int -> Int -> ByteArray -> Maybe# Int#+findNonAscii' !start !len !arr = findVectorizable+  (\w -> w .&. asciiMask /= 0)+  (\w -> w .&. asciiMachMask /= 0)+  start len arr++-- | The meaning of the result sum elements in order:+--+--   1. An integer representing the first unparseable byte. This+--      may be up to three bytes before the actual byte where+--      the actual problem occurred. The leading byte is given+--      since that gives recovery mechanisms a reasonable place+--      to split the Bytes.+--   2. An empty nullary tuple indicating success with no leftovers+--   3. A triple of three words. The first word is the total number+--      of bytes in the multibyte word (only ever 2, 3, or 4). The+--      second is the number of additional bytes needed (only+--      ever 1, 2, or 3). The third is the fragment of the character built+--      so far. It will need to be bit shifted to the left by some+--      multiple of 6 to be completed.+--   +--   The first tuple element is 0 if everything successfully+--   parsed was acsii and a word with the high bit set to 1+--   if multi-byte characters were present.+--   It is 1 if there were surrogates, characters+--   in the range @U+D800@ to @U+DFFF@, present in the text. If+--   there are surrogates, it is implied that there are multi-byte+--   characters, since a surrogate is multi-byte by definition.+--   The value of this element is still meaningful even if the parse+--   ultimately fails.+isUtf8 :: +     Int -- start+  -> Int -- length+  -> ByteArray -- bytes+  -> (# Word#, (# Int# | (# #) | (# Word#, Word#, Word# #) #) #)+isUtf8 !start !len !arr = case findNonAscii' start len arr of+  (# (# #) | #) -> (# 0##, (# | (# #) | #) #)+  (# | ix# #) -> case postAsciiIsUtf8 (I# ix#) (len + start - (I# ix#)) arr of+    (# hasSurrogate, (# ixFailure# | | #) #) -> if I# ix# == I# ixFailure#+      then (# 0## , (# ixFailure# | | #) #)+      else (# hasSurrogate, (# ixFailure# | | #) #)+    (# hasSurrogate, (# | (# #) | #) #) -> (# hasSurrogate, (# | (# #) | #) #)+    (# hasSurrogate, (# | | (# w1, w2, w3 #) #) #) -> (# hasSurrogate, (# | | (# w1, w2, w3 #) #) #)+-- Notes on the implementation of isUtf8 There is some careful trickery to+-- ensure that we always correctly report whether or not we encountered any+-- multi-byte characters. We initially do a fast run to get as far as we can+-- on only ascii characters. In this stage, we are able to travel a full+-- machine word at a time. After this, we switch to a slower byte-by-byte+-- UTF-8 recognition function.  If this second stage fails on the very first+-- byte it sees, we report that everything successfully parsed was ascii.+-- If it fails at any point after this, we report that multibyte characters+-- were encounter. If it succeeds, we report that we encounter multi-byte+-- characters. This is accurate because, for it to start running at all,+-- findNonAscii' must have found something that was not ascii.++-- Note that postAsciiIsUtf8 does not return anything indicating whether or not there+-- were any multi-byte characters present. This is because, if this function+-- is called, it should be called with the start index on a non-ascii-encoded+-- character. This function is not expect to perform well. It shouldn't+-- allocate memory, but it has to go byte-by-byte through the ByteArray. The+-- author of this library does not know of a way to vectorize the check+-- for UTF-8 compliance.+--+-- The first element of the response tuple is either 1 or a machine word+-- with the high bit set to 1. If it is the high bit,+-- no surrogates were present. If it is 1, surrogates were present.+-- The second element is a nested unboxed sum with three cases. These+-- are described in the docs for isUtf8.+--+postAsciiIsUtf8 :: +     Int -- start+  -> Int -- length+  -> ByteArray -- bytes+  -> (# Word#, (# Int# | (# #) | (# Word#, Word#, Word# #) #) #)+postAsciiIsUtf8 !start !len !arr = go start binaryOneThenZeroes+  where+  !end = start + len+  go :: Int +     -> Word+     -> (# Word#, (# Int# | (# #) | (# Word#, Word#, Word# #) #) #)+  go !ix !hasSurrogate = if ix < end+    then+      let !firstByte = safeIndex arr ix in+       if | oneByteChar firstByte -> go (ix + 1) hasSurrogate+          | twoByteChar firstByte -> if ix + 1 < end+              then if followingByte (safeIndex arr (ix + 1))+                then go (ix + 2) hasSurrogate+                else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+              else (# unboxWord hasSurrogate, (# | | (# 2##, 1##, unboxWord (byteTwoPartialOne firstByte) #) #) #)+          | threeByteChar firstByte ->+              if | ix + 2 < end -> +                     let !secondByte = safeIndex arr (ix + 1) in+                     if followingByte secondByte+                       then +                         let !thirdByte = safeIndex arr (ix + 2) in+                         if followingByte thirdByte+                           then if surrogate (codepointFromThreeBytes firstByte secondByte thirdByte)+                             then go (ix + 3) 1+                             else go (ix + 3) hasSurrogate+                           else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                       else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                 | ix + 1 < end -> +                     let !secondByte = safeIndex arr (ix + 1) in+                     if followingByte secondByte+                       then (# unboxWord hasSurrogate, (# | | (# 3##, 1##, unboxWord (byteThreePartialTwo firstByte secondByte) #) #) #)+                       else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                 | otherwise -> (# unboxWord hasSurrogate, (# | | (# 3##, 2##, unboxWord (byteThreePartialOne firstByte) #) #) #)+          | fourByteChar firstByte ->+              if | ix + 3 < end ->+                     let !secondByte = safeIndex arr (ix + 1) in+                     if followingByte secondByte+                       then +                         let !thirdByte = safeIndex arr (ix + 2) in+                         if followingByte thirdByte+                           then+                             let !fourthByte = safeIndex arr (ix + 3) in+                             if followingByte fourthByte+                               then go (ix + 4) hasSurrogate+                               else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                           else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                       else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                 | ix + 2 < end -> +                     let !secondByte = safeIndex arr (ix + 1) in+                     if followingByte secondByte+                       then +                         let !thirdByte = safeIndex arr (ix + 2) in+                         if followingByte thirdByte+                           then (# unboxWord hasSurrogate, (# | | (# 4##, 1##, unboxWord (byteFourPartialThree firstByte secondByte thirdByte) #) #) #)+                           else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                       else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                 | ix + 1 < end -> +                     let !secondByte = safeIndex arr (ix + 1) in+                     if followingByte secondByte+                       then (# unboxWord hasSurrogate, (# | | (# 4##, 2##, unboxWord (byteFourPartialTwo firstByte secondByte) #) #) #)+                       else (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+                 | otherwise -> (# unboxWord hasSurrogate, (# | | (# 4##, 3##, unboxWord (byteFourPartialOne firstByte) #) #) #)+          | otherwise -> (# unboxWord hasSurrogate, (# unboxInt ix | | #) #)+    else (# unboxWord hasSurrogate, (# | (# #) | #) #)++byteTwoPartialOne :: Word8 -> Word+byteTwoPartialOne w = word8ToWord w .&. 0b00011111++byteThreePartialTwo :: Word8 -> Word8 -> Word+byteThreePartialTwo a b =+  unsafeShiftL (word8ToWord a .&. 0b00001111) 6 .|.+  (word8ToWord b .&. 0b00111111)++byteThreePartialOne :: Word8 -> Word+byteThreePartialOne a = word8ToWord a .&. 0b00001111++byteFourPartialTwo :: Word8 -> Word8 -> Word+byteFourPartialTwo a b =+  unsafeShiftL (word8ToWord a .&. 0b00000111) 6 .|.+  (word8ToWord b .&. 0b00111111)++byteFourPartialThree :: Word8 -> Word8 -> Word8 -> Word+byteFourPartialThree a b c =+  unsafeShiftL (word8ToWord a .&. 0b00000111) 12 .|.+  unsafeShiftL (word8ToWord b .&. 0b00111111) 6 .|.+  (word8ToWord c .&. 0b00111111)++byteFourPartialOne :: Word8 -> Word+byteFourPartialOne a = word8ToWord a .&. 0b00000111++followingByte :: Word8 -> Bool+followingByte !w = xor w 0b01000000 .&. 0b11000000 == 0b11000000++oneByteChar :: Word8 -> Bool+oneByteChar !w = w .&. 0b10000000 == 0++twoByteChar :: Word8 -> Bool+twoByteChar !w = w .&. 0b11100000 == 0b11000000++threeByteChar :: Word8 -> Bool+threeByteChar !w = w .&. 0b11110000 == 0b11100000++fourByteChar :: Word8 -> Bool+fourByteChar !w = w .&. 0b11111000 == 0b11110000++codepointFromThreeBytes :: Word8 -> Word8 -> Word8 -> Word+codepointFromThreeBytes w1 w2 w3 = +  unsafeShiftL (word8ToWord w1 .&. 0b00001111) 12 .|. +  unsafeShiftL (word8ToWord w2 .&. 0b00111111) 6 .|. +  (word8ToWord w3 .&. 0b00111111)++word8ToWord :: Word8 -> Word+word8ToWord = fromIntegral++surrogate :: Word -> Bool+surrogate codepoint = codepoint >= 0xD800 && codepoint < 0xE000++binaryOneThenZeroes :: Word+binaryOneThenZeroes = maxBound - div (maxBound :: Word) 2+
+ src/Packed/Text.hs view
@@ -0,0 +1,600 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BinaryLiterals #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++{-# OPTIONS_GHC+ -Weverything+ -fno-warn-unsafe+ -fno-warn-implicit-prelude+ -fno-warn-missing-import-lists+ -O2+#-}++module Packed.Text+  ( Text(..)+  , empty+  , pack+  , unpack+  , breakChar+  , map+  , toUpper+  , take+  , drop+  , dropEnd+  , length+    -- * Encoding+  , decodeAscii+  , decodeResumeAscii+  , decodeUtf8+  , decodeResumeUtf8+  , encodeUtf8+  ) where++import Prelude hiding (map,take,drop,length)+import Data.Char (ord,chr)+import Data.Primitive (MutableByteArray)+import Packed.Bytes.Small (ByteArray)+import GHC.Exts (Word#)+import GHC.Int (Int(I#))+import GHC.Word (Word(W#),Word8(W8#))+import Data.Bits ((.&.),(.|.),unsafeShiftR,unsafeShiftL,complement)+import Control.Monad.ST (ST,runST)+import Packed.Bytes (Bytes(..))+import qualified Data.Char+import qualified Packed.Bytes.Small as BA+import qualified Packed.Bytes.Window as BAW+import qualified Packed.Bytes as B+import qualified Data.Primitive as PM++data Text = Text+  {-# UNPACK #-} !ByteArray -- payload, normal UTF8-encoded text, nothing special like the unsliced variant+  {-# UNPACK #-} !Word -- offset in bytes, not in characters, first bit reserved+  {-# UNPACK #-} !Int -- length in bytes, not in characters+-- The byte-multiplicity for text is always Multiple if+-- a multi-byte character is present. If no multi-byte+-- characters are present, it may be set to either single+-- or multiple. Functions should try to set it to single+-- when possible.++instance Eq Text where+  t1 == t2 = Bytes arr1 off1 len1 == Bytes arr2 off2 len2+    where+    !(!arr1,!off1,!len1,!_) = textMatch t1+    !(!arr2,!off2,!len2,!_) = textMatch t2++instance Show Text where+  show = show . unpack ++newtype Multiplicity = Multiplicity Word+  deriving Eq++appendMult :: Multiplicity -> Multiplicity -> Multiplicity+appendMult (Multiplicity a) (Multiplicity b) = Multiplicity (a .|. b)++single :: Multiplicity+single = Multiplicity 0++multiple :: Multiplicity+multiple = Multiplicity binaryOneThenZeroes++pack :: String -> Text+pack str = case metadata 0 single str of+  (!totalBytes,!totalMult) -> +    let !arr = runST $ do+          marr <- PM.newByteArray totalBytes+          let go [] !_ = return ()+              go (!c : cs) !ix0 = do+                ix1 <- writeChar c ix0 marr+                go cs ix1+          go str 0+          PM.unsafeFreezeByteArray marr+     in Text arr (buildZeroOffMult totalMult) totalBytes+  where+  metadata :: Int -> Multiplicity -> [Char] -> (Int,Multiplicity)+  metadata !totalBytes !totalMult [] = (totalBytes,totalMult)+  metadata !totalBytes !totalMult (!c : cs) =+    let !bytes = charBytes c+        !mult = if bytes < 2 then single else multiple+     in metadata (bytes + totalBytes) (appendMult mult totalMult) cs++-- Result is between 1 and 4. The guards used here do not have to treat+-- surrogates as a special case.+charBytes :: Char -> Int+charBytes !c+  | codepoint < 0x80 = 1+  | codepoint < 0x800 = 2+  | codepoint < 0x10000 = 3+  | otherwise = 4+  where+  !codepoint = intToWord (ord c)++-- returns the new index+writeChar :: Char -> Int -> PM.MutableByteArray s -> ST s Int+writeChar !c !ix !marr+  | codepoint < 0x80 = do+      PM.writeByteArray marr ix (unsafeWordToWord8 codepoint)+      return (ix + 1)+  | codepoint < 0x800 = do+      PM.writeByteArray marr ix (unsafeWordToWord8 (byteTwoOne codepoint))+      PM.writeByteArray marr (ix + 1) (unsafeWordToWord8 (byteTwoTwo codepoint))+      return (ix + 2)+  | surrogate codepoint = do+      -- Codepoint U+FFFD+      PM.writeByteArray marr ix (0xEF :: Word8)+      PM.writeByteArray marr (ix + 1) (0xBF :: Word8)+      PM.writeByteArray marr (ix + 2) (0xBD :: Word8)+      return (ix + 3)+  | codepoint < 0x10000 = do+      PM.writeByteArray marr ix (unsafeWordToWord8 (byteThreeOne codepoint))+      PM.writeByteArray marr (ix + 1) (unsafeWordToWord8 (byteThreeTwo codepoint))+      PM.writeByteArray marr (ix + 2) (unsafeWordToWord8 (byteThreeThree codepoint))+      return (ix + 3)+  | otherwise = do+      PM.writeByteArray marr ix (unsafeWordToWord8 (byteFourOne codepoint))+      PM.writeByteArray marr (ix + 1) (unsafeWordToWord8 (byteFourTwo codepoint))+      PM.writeByteArray marr (ix + 2) (unsafeWordToWord8 (byteFourThree codepoint))+      PM.writeByteArray marr (ix + 3) (unsafeWordToWord8 (byteFourFour codepoint))+      return (ix + 4)+  where+  !codepoint = intToWord (ord c)++unpack :: Text -> String+unpack !t = go off+  where+  go :: Int -> String+  go !ix0 = if ix0 < len + off+    then+      let !(!ix1,!c) = nextChar arr ix0+       in c : go ix1+    else []+  !(!arr,!off,!len,!_) = textMatch t++nextChar :: ByteArray -> Int -> (Int,Char)+nextChar !arr !ix+  | oneByteChar firstByte = (ix + 1, wordToChar (word8ToWord firstByte))+  | twoByteChar firstByte =+      let !secondByte = BA.unsafeIndex arr (ix + 1)+       in (ix + 2, charFromTwoBytes firstByte secondByte)+  | threeByteChar firstByte =+      let !secondByte = BA.unsafeIndex arr (ix + 1)+          !thirdByte = BA.unsafeIndex arr (ix + 2)+       in (ix + 3, charFromThreeBytes firstByte secondByte thirdByte)+  | otherwise =+      let !secondByte = BA.unsafeIndex arr (ix + 1)+          !thirdByte = BA.unsafeIndex arr (ix + 2)+          !fourthByte = BA.unsafeIndex arr (ix + 3)+       in (ix + 4, charFromFourBytes firstByte secondByte thirdByte fourthByte)+  where+  firstByte :: Word8+  !firstByte = BA.unsafeIndex arr ix++nextCharIx :: ByteArray -> Int -> Int+nextCharIx !arr !ix+  | oneByteChar firstByte = ix + 1+  | twoByteChar firstByte = ix + 2+  | threeByteChar firstByte = ix + 3+  | otherwise = ix + 4+  where+  firstByte :: Word8+  !firstByte = BA.unsafeIndex arr ix++moveChars ::+     ByteArray -- array+  -> Int -- start index+  -> Int -- maximal index+  -> Int -- number of characters to move through+  -> Int -- end index+moveChars !arr !start0 !maxIndex !n0 = go start0 n0+  where+  go :: Int -> Int -> Int+  go !ix !n = if n > 0 && ix < maxIndex+    then go (nextCharIx arr ix) (n - 1)+    else ix++countChars ::+     ByteArray -- array+  -> Int -- start index+  -> Int -- maximal index+  -> Int -- number of characters +countChars !arr !start0 !maxIndex = go start0 0+  where+  go :: Int -> Int -> Int+  go !ix !acc = if ix < maxIndex+    then go (nextCharIx arr ix) (acc + 1)+    else acc++oneByteChar :: Word8 -> Bool+oneByteChar w = w .&. 0b10000000 == 0++twoByteChar :: Word8 -> Bool+twoByteChar w = w .&. 0b11100000 == 0b11000000++threeByteChar :: Word8 -> Bool+threeByteChar w = w .&. 0b11110000 == 0b11100000++charFromTwoBytes :: Word8 -> Word8 -> Char+charFromTwoBytes w1 w2 = wordToChar $+  unsafeShiftL (word8ToWord w1 .&. 0b00011111) 6 .|. +  (word8ToWord w2 .&. 0b00111111)++charFromThreeBytes :: Word8 -> Word8 -> Word8 -> Char+charFromThreeBytes w1 w2 w3 = wordToChar $+  unsafeShiftL (word8ToWord w1 .&. 0b00001111) 12 .|. +  unsafeShiftL (word8ToWord w2 .&. 0b00111111) 6 .|. +  (word8ToWord w3 .&. 0b00111111)++charFromFourBytes :: Word8 -> Word8 -> Word8 -> Word8 -> Char+charFromFourBytes w1 w2 w3 w4 = wordToChar $+  unsafeShiftL (word8ToWord w1 .&. 0b00000111) 18 .|. +  unsafeShiftL (word8ToWord w2 .&. 0b00111111) 12 .|. +  unsafeShiftL (word8ToWord w3 .&. 0b00111111) 6 .|. +  (word8ToWord w4 .&. 0b00111111)++-- precondition: codepoint is less than 0x800+byteTwoOne :: Word -> Word+byteTwoOne w = unsafeShiftR w 6 .|. 0b11000000++byteTwoTwo :: Word -> Word+byteTwoTwo w = (w .&. 0b00111111) .|. 0b10000000++-- precondition: codepoint is less than 0x1000+byteThreeOne :: Word -> Word+byteThreeOne w = unsafeShiftR w 12 .|. 0b11100000++byteThreeTwo :: Word -> Word+byteThreeTwo w = (0b00111111 .&. unsafeShiftR w 6) .|. 0b10000000++byteThreeThree :: Word -> Word+byteThreeThree w = (w .&. 0b00111111) .|. 0b10000000++-- precondition: codepoint is less than 0x110000+byteFourOne :: Word -> Word+byteFourOne w = unsafeShiftR w 18 .|. 0b11110000++byteFourTwo :: Word -> Word+byteFourTwo w = (0b00111111 .&. unsafeShiftR w 12) .|. 0b10000000++byteFourThree :: Word -> Word+byteFourThree w = (0b00111111 .&. unsafeShiftR w 6) .|. 0b10000000++byteFourFour :: Word -> Word+byteFourFour w = (0b00111111 .&. w) .|. 0b10000000++++word8ToWord :: Word8 -> Word+word8ToWord = fromIntegral++intToWord :: Int -> Word+intToWord = fromIntegral++wordToInt :: Word -> Int+wordToInt = fromIntegral++unsafeWordToWord8 :: Word -> Word8+unsafeWordToWord8 (W# w) = W8# w++wordToChar :: Word -> Char+wordToChar w = chr (fromIntegral w)++empty :: Text+empty = Text BA.empty 0 0++breakChar :: Char -> Text -> (Text,Text)+breakChar !c !t+  | codepoint < 0x80 = breakOnByte1 (unsafeWordToWord8 codepoint) t+  | codepoint < 0x800 = case findBytePair off len (unsafeWordToWord8 (byteTwoOne codepoint)) (unsafeWordToWord8 (byteTwoTwo codepoint)) arr of+      Nothing -> (t,empty)+      Just ix -> (Text arr (buildOffMult off mult) (ix - off), Text arr (buildOffMult ix mult) (len + off - ix))+  | surrogate codepoint = case findByteTriple off len 0xEF 0xBF 0xBD arr of+      Nothing -> (t,empty)+      Just ix -> (Text arr (buildOffMult off mult) (ix - off), Text arr (buildOffMult ix mult) (len + off - ix))+  | codepoint < 0x10000 = case findByteTriple off len (unsafeWordToWord8 (byteThreeOne codepoint)) (unsafeWordToWord8 (byteThreeTwo codepoint)) (unsafeWordToWord8 (byteThreeThree codepoint)) arr of+      Nothing -> (t,empty)+      Just ix -> (Text arr (buildOffMult off mult) (ix - off), Text arr (buildOffMult ix mult) (len + off - ix))+  | otherwise = case findByteQuadruple off len (unsafeWordToWord8 (byteFourOne codepoint)) (unsafeWordToWord8 (byteFourTwo codepoint)) (unsafeWordToWord8 (byteFourThree codepoint)) (unsafeWordToWord8 (byteFourFour codepoint)) arr of+      Nothing -> (t,empty)+      Just ix -> (Text arr (buildOffMult off mult) (ix - off), Text arr (buildOffMult ix mult) (len + off - ix))+  where+  !codepoint = intToWord (ord c)+  !(!arr,!off,!len,!mult) = textMatch t++findBytePair :: Int -> Int -> Word8 -> Word8 -> ByteArray -> Maybe Int+findBytePair off0 len0 w1 w2 arr = go off0 (off0 + len0 - 1)+  where+  go :: Int -> Int -> Maybe Int+  go !ix !end = if ix < end+    then if PM.indexByteArray arr ix == w1 && PM.indexByteArray arr (ix + 1) == w2+      then Just ix+      else go (ix + 1) end+    else Nothing++findByteTriple :: Int -> Int -> Word8 -> Word8 -> Word8 -> ByteArray -> Maybe Int+findByteTriple off0 len0 w1 w2 w3 arr = go off0 (off0 + len0 - 2)+  where+  go :: Int -> Int -> Maybe Int+  go !ix !end = if ix < end+    then if PM.indexByteArray arr ix == w1 && PM.indexByteArray arr (ix + 1) == w2 && PM.indexByteArray arr (ix + 2) == w3+      then Just ix+      else go (ix + 1) end+    else Nothing++findByteQuadruple :: Int -> Int -> Word8 -> Word8 -> Word8 -> Word8 -> ByteArray -> Maybe Int+findByteQuadruple off0 len0 w1 w2 w3 w4 arr = go off0 (off0 + len0 - 3)+  where+  go :: Int -> Int -> Maybe Int+  go !ix !end = if ix < end+    then if PM.indexByteArray arr ix == w1 && PM.indexByteArray arr (ix + 1) == w2 && PM.indexByteArray arr (ix + 2) == w3 && PM.indexByteArray arr (ix + 3) == w4+      then Just ix+      else go (ix + 1) end+    else Nothing++-- precondition: the Word is less than 0x80+breakOnByte1 :: Word8 -> Text -> (Text,Text)+breakOnByte1 !w !t =+  let !(!arr,!off,!len,!mult) = textMatch t+   in case BAW.findByte off len w arr of+        Nothing -> (t,empty)+        Just !ix -> (dwindle (Text arr (buildOffMult off mult) (ix - off)), Text arr (buildOffMult ix mult) (len + off - ix))++dwindle :: Text -> Text+dwindle t@(Text _ _ !len) = if len > 0 then t else empty+  +buildOffMult :: Int -> Multiplicity -> Word+buildOffMult i (Multiplicity x) = intToWord i .|. x++buildZeroOffMult :: Multiplicity -> Word+buildZeroOffMult (Multiplicity w) = w++textMatch :: Text -> (ByteArray,Int,Int,Multiplicity)+textMatch (Text arr offMult len) =+  ( arr+  , wordToInt (binaryZeroThenOnes .&. offMult)+  , len+  , Multiplicity (offMult .&. binaryOneThenZeroes)+  )++binaryOneThenZeroes :: Word+binaryOneThenZeroes = maxBound - div (maxBound :: Word) 2++binaryZeroThenOnes :: Word+binaryZeroThenOnes = div (maxBound :: Word) 2++surrogate :: Word -> Bool+surrogate codepoint = codepoint >= 0xD800 && codepoint < 0xE000++-- Each byte in the word that is a lowercase ascii character is turned+-- into 0x80. All other bytes become 0x00.+hasAsciiLowerArtifact :: Word -> Word+hasAsciiLowerArtifact w = +  ((div maxBound 255 * (127 + hi)) - (w .&. (div maxBound 255 * 127))) .&. complement w .&.+  ((w .&. (div maxBound 255 * 127)) + (div maxBound 255 * (127 - lo))) .&. (div maxBound 255 * 128)+  where+  lo = intToWord (ord 'a' - 1)+  hi = intToWord (ord 'z' + 1)++-- TODO: improve this. Currently, we do not use a vectorized implementation if+-- the string doesn't start aligned on a machine word address. We can fix this+-- by padding the result to the left to line it up better. This would trade a+-- small amount of space for more speed.+{-# INLINE mapVectorizable #-}+mapVectorizable ::+     (Word8 -> Word8) -- function+  -> (Word -> Word) -- vectorized function variant+  -> Int -- start+  -> Int -- len+  -> ByteArray -- array+  -> ByteArray+mapVectorizable !func !funcMach !start !len !arr = runST action+  where+  action :: forall s. ST s ByteArray+  action = do+    marr <- PM.newByteArray len+    let !(!quotStart,!remStart) = quotRem start (PM.sizeOf (undefined :: Word))+        go :: Int -> Int -> ST s ()+        go !ix !end = if ix < end+          then do+            PM.writeByteArray marr ix (func (PM.indexByteArray arr (start + ix)))+            go (ix + 1) end+          else return ()+        goMach :: Int -> Int -> ST s ()+        goMach !ix !end = if ix < end+          then do+            PM.writeByteArray marr ix (funcMach (PM.indexByteArray arr (quotStart + ix)))+            goMach (ix + 1) end+          else return ()+    if remStart == 0+      then do+        let !lenQuotient = quot len (PM.sizeOf (undefined :: Word))+        goMach 0 lenQuotient+        go (lenQuotient * PM.sizeOf (undefined :: Word)) len+      else go 0 len+    PM.unsafeFreezeByteArray marr++toUpperAsciiWord8 :: Word8 -> Word8+toUpperAsciiWord8 w = if word8ToWord w - intToWord (ord 'a') < 26+  then w - 0x20+  else w++toUpperAsciiWord :: Word -> Word+toUpperAsciiWord w = w - unsafeShiftR (hasAsciiLowerArtifact w) 2++toUpperAscii :: Int -> Int -> ByteArray -> ByteArray+toUpperAscii !off !len !arr = mapVectorizable toUpperAsciiWord8 toUpperAsciiWord off len arr++toUpper :: Text -> Text+toUpper t = if mult == single+  then Text (toUpperAscii off len arr) (buildZeroOffMult single) len+  else map Data.Char.toUpper t+  where+  !(!arr,!off,!len,!mult) = textMatch t++map :: (Char -> Char) -> Text -> Text+map f !t = runST action+  where+  !(!arr,!off,!len,!_) = textMatch t+  action :: ST s Text+  action = do+    marr0 <- PM.newByteArray (len + 3)+    let go :: Int -> Int -> Int -> Multiplicity -> MutableByteArray s -> ST s (Int,MutableByteArray s,Multiplicity)+        go !ixSrc !ixDst !marrLen !mult !marr = if ixSrc < off + len+          then do+            let !(!ixSrcNext,!c) = nextChar arr ixSrc+                !c' = f c+                -- It is disappointing that this is handled this+                -- way. Reconsider this later.+                !newMult = appendMult mult (if ixSrcNext - ixSrc > 1 then multiple else single)+            if ixDst < marrLen - 3+              then do+                ixDstNext <- writeChar c' ixDst marr+                go ixSrcNext ixDstNext marrLen newMult marr+              else do+                let newMarrLen = marrLen * 2+                newMarr <- PM.newByteArray newMarrLen+                PM.copyMutableByteArray newMarr 0 marr 0 marrLen -- possible minus 3?+                ixDstNext <- writeChar c' ixDst newMarr+                go ixSrcNext ixDstNext newMarrLen newMult newMarr+          else return (ixDst,marr,mult)+    (finalLen,finalMarr,finalMult) <- go off 0 (len + 3) single marr0+    newArr <- PM.unsafeFreezeByteArray finalMarr+    return (Text newArr (buildZeroOffMult finalMult) finalLen)++-- | /O(n)/ 'take' @n xs@ returns the prefix of @xs@ of length @n@. It returns+--   @xs@ instead when @n > 'length' xs@. On text containing only ASCII characters,+--   the complexity of this function is reduced to /O(1)/.+take :: Int -> Text -> Text+take !n !t@(Text _ !offMult _) = if n < 1+  then empty+  else if mult == single+    then if n < len+      then Text arr offMult n+      else t+    else if n < len * 4+      then Text arr offMult (moveChars arr off (off + len) n - off)+      else t+  where+  !(!arr,!off,!len,!mult) = textMatch t++-- | /O(n)/ 'drop' @n xs@ returns the suffix of @xs@ after the first @n@ characters+--   have been removed. It returns @empty@ instead when @n > 'length' xs@. On text+--   containing only ASCII characters, the complexity of this function is reduced to /O(1)/.+drop :: Int -> Text -> Text+drop !n !t = if n < 1+  then t+  else if mult == single+    then if n < len+      then Text arr (buildOffMult (off + n) mult) (len - n)+      else empty+    else if n < len * 4+      then+        let !skipped = moveChars arr off (off + len) n - off+         in Text arr (buildOffMult (off + skipped) mult) (len - skipped)+      else empty+  where+  !(!arr,!off,!len,!mult) = textMatch t++length :: Text -> Int+length !t = if mult == single+  then len+  else countChars arr off (off + len)+  where+  !(!arr,!off,!len,!mult) = textMatch t++-- | /O(n)/ 'dropEnd' @n xs@ returns the prefix of @xs@ after the last @n@ characters+--   have been removed. It returns @empty@ instead when @n > 'length' xs@. On text+--   containing only ASCII characters, the complexity of this function is reduced to /O(1)/.+dropEnd :: Int -> Text -> Text+-- Note: There is a way to implement this that is more efficient. It would+-- required scanning UTF-8 encoded text backwards, which seems annoying+-- to do.+dropEnd !n !t = take (length t - n) t++decodeAscii :: Bytes -> Maybe Text+decodeAscii b = case decodeResumeAscii b of+  (# t, (# | (# #) #) #) -> Just t+  (# !_, (# !_ | #) #) -> Nothing+-- decodeAscii b@(Bytes arr off len) = if B.isAscii b+--   then Just (Text arr (buildOffMult off single) len)+--   else Nothing++decodeResumeAscii :: Bytes -> (# Text, (# Bytes | (# #) #) #)+decodeResumeAscii (Bytes arr off len) = case BAW.findNonAscii' off len arr of+  (# (# #) | #) -> (# Text arr (buildOffMult off single) len, (# | (# #) #) #)+  (# | ix# #) -> +    let ix = I# ix#+     in (# Text arr (buildOffMult off single) ix, (# (Bytes arr ix (len + off - ix)) | #) #)++decodeUtf8 :: Bytes -> Maybe Text+decodeUtf8 b = case decodeResumeUtf8 b of+  (# !_, (# !_ | | #) #) -> Nothing+  (# !t, (# | (# #) | #) #) -> Just t+  (# !_, (# | | !_ #) #) -> Nothing++-- decodeUtf8 :: Bytes -> Maybe Text+-- decodeUtf8 b = case decodeResumeUtf8 b of+--   (# !_, West !_ #) -> Nothing+--   (# !t, Center () #) -> Just t+--   (# !_, East !_ #) -> Nothing++data Choice a b c = West a | Center b | East c++decodeResumeUtf8 ::+     Bytes+  -> (# Text, (# Bytes | (# #) | (# Word#, Word#, Word# #) #) #)+decodeResumeUtf8 (Bytes arr off len) = +  let !(# !ascii, !r #) = BAW.isUtf8 off len arr+      !modifiedArr = case ascii of+        1## -> runST $ do+          marr <- PM.newByteArray len+          PM.copyByteArray marr 0 arr off len+          replaceSurrogates 0 len marr+          PM.unsafeFreezeByteArray marr+        _ -> arr+      !mult = case ascii of+        1## -> multiple+        _ -> Multiplicity (W# ascii)+   in case r of+        (# ixFailure# | | #) -> +          let !ixFailure = I# ixFailure#+           in (# Text modifiedArr (buildOffMult off mult) ixFailure, (# (Bytes arr ixFailure (len + off - ixFailure)) | | #) #)+        (# | (# #) | #) -> (# Text modifiedArr (buildOffMult off mult) len, (# | (# #) | #) #)+        (# | | (# w1, w2, w3 #) #) -> (# Text modifiedArr (buildOffMult off mult) len , (# | | (# w1, w2, w3 #) #) #)++replaceSurrogates :: forall s. Int -> Int -> MutableByteArray s -> ST s ()+replaceSurrogates start len marr = go start where+  go :: Int -> ST s ()+  go !ix = if ix < len - 2+    then do+      !w1 <- PM.readByteArray marr ix+      !w2 <- PM.readByteArray marr (ix + 1)+      !w3 <- PM.readByteArray marr (ix + 2)+      if threeByteChar w1 && surrogate (codepointFromThreeBytes w1 w2 w3)+        then do+          -- Codepoint U+FFFD+          PM.writeByteArray marr ix (0xEF :: Word8)+          PM.writeByteArray marr (ix + 1) (0xBF :: Word8)+          PM.writeByteArray marr (ix + 2) (0xBD :: Word8)+          go (ix + 3)+        else go (ix + 1)+    else return ()++codepointFromThreeBytes :: Word8 -> Word8 -> Word8 -> Word+codepointFromThreeBytes w1 w2 w3 = +  unsafeShiftL (word8ToWord w1 .&. 0b00001111) 12 .|. +  unsafeShiftL (word8ToWord w2 .&. 0b00111111) 6 .|. +  (word8ToWord w3 .&. 0b00111111)++convertTuple :: (# (# #) | (# Word#, Word#, Word# #) #) -> Either () (Word,Word,Word)+convertTuple (# (# #) | #) = Left ()+convertTuple (# | w #) = Right (convertWordTuple w)++convertWordTuple :: (# Word#, Word#, Word# #) -> (Word,Word,Word)+convertWordTuple (# a,b,c #) = ( W# a, W# b, W# c )++encodeUtf8 :: Text -> Bytes+encodeUtf8 t = Bytes arr off len where+  !(!arr,!off,!len,!_) = textMatch t+
+ src/Packed/Text/Small.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BinaryLiterals #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE ScopedTypeVariables #-}++{-# OPTIONS_GHC+ -Weverything+ -fno-warn-unsafe+ -fno-warn-implicit-prelude+ -fno-warn-missing-import-lists+ -O2+#-}+module Packed.Text.Small+  ( SmallText(..)+  , empty+  , reverse+  , decodeAscii+  ) where++import Prelude hiding (reverse)++import Packed.Bytes.Small (ByteArray)+import Data.Bits ((.&.))+import Data.Semigroup (Semigroup)+import qualified Packed.Bytes.Small as BA+import qualified Data.Semigroup as SG++newtype SmallText = SmallText ByteArray ++-- Text is UTF-8 encoded with one caveat. There are several operations+-- that can be faster if we know in advance that all the codepoints in+-- a piece of text are in the ascii range. So, we use the first+-- byte to encode the additional information. If the first byte has+-- zero as the MSB (representing a code point in the ascii range),+-- it is assumed that the entire piece of text only uses code points+-- in the ascii range. If however, the first byte is 0b11111111+-- (which is not valid a UTF-8 byte anywhere), then this first byte is discarded+-- and it is assumed that the text contains code points from any range.+-- The first byte is not allowed to be anything starting with 1.+-- This bloats all non-English text by one byte.++instance Eq SmallText where+  SmallText a == SmallText b = a == b++-- This is a boolean but with a more efficient way to+-- do conjunction+newtype UnicodeRange = UnicodeRange Word++ascii :: UnicodeRange+ascii = UnicodeRange 1++nonAscii :: UnicodeRange+nonAscii = UnicodeRange 0++instance Semigroup UnicodeRange where+  UnicodeRange a <> UnicodeRange b = UnicodeRange (a .&. b)++instance Monoid UnicodeRange where+  mempty = ascii+  mappend = (SG.<>)++unicodeRange :: ByteArray -> UnicodeRange+unicodeRange arr+  | BA.length arr > 0 =+      case BA.unsafeIndex arr 0 of+        0b11111111 -> nonAscii+        _ -> ascii+  | otherwise = ascii++empty :: SmallText+empty = SmallText BA.empty++reverse :: SmallText -> SmallText+reverse (SmallText arr) = case unicodeRange arr of+  UnicodeRange 1 -> SmallText (BA.reverse arr)+  _ -> error "Text.Array.reverse: write the non-ascii case"++decodeAscii :: ByteArray -> Maybe SmallText+decodeAscii arr = if BA.isAscii arr+  then Just (SmallText arr)+  else Nothing++
+ test/Main.hs view
@@ -0,0 +1,325 @@+{-# LANGUAGE BinaryLiterals #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}+{-# LANGUAGE MagicHash #-}++import Data.Set (Set)+import Data.Word (Word8)+import Hedgehog (Property,Gen,property,forAll,(===),failure)+import Hedgehog.Gen (list,enumBounded,int,frequency,choice,element,integral,word8)+import Hedgehog.Range (Range,linear)+import Test.Tasty (defaultMain,testGroup,TestTree)+import Data.Bits ((.&.))+import Data.Char (chr)+import Test.Tasty.HUnit (testCase)+import Test.Tasty.Hedgehog (testProperty)+import Data.Bifunctor (bimap)+import Data.Monoid+import GHC.Exts (Int#)+import GHC.Types+import Packed.Bytes (Bytes)+import Packed.Bytes.Small (ByteArray)+import GHC.Int (Int(I#))+import Data.Bits ((.&.),(.|.),unsafeShiftR)++import qualified Data.Char+import qualified Test.Tasty.Hedgehog as H+import qualified Packed.Text as T+import qualified Packed.Bytes.Small as BA+import qualified Packed.Bytes.Window as BAW+import qualified Packed.Bytes as B+import qualified Data.Set as S+import qualified GHC.OldList as L++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests"+  [ testGroup "ByteArray"+    [ testProperty "findByte" findByteProp+    , testProperty "zipAnd" zipAndProp+    ]+  , testGroup "Bytes"+    [ testProperty "findByte" sliceFindByteProp+    ]+  , testGroup "Text"+    [ testProperty "pack" textPackProp+    , testProperty "take" textTakeProp+    , testProperty "drop" textDropProp+    , testProperty "breakOnChar" textBreakCharProp+    , testProperty "toUpper" textToUpperProp+    , testProperty "decodeAscii" textDecodeAscii+    , testGroup "decodeUtf8"+      [ testProperty "isomorphism" textDecodeUtf8Iso+      , testProperty "surrogates" textDecodeUtf8Surrogates+        -- TODO: test against malformed inputs to decodeUtf8+      ]+    ]+  ]++isAscii :: Word8 -> Bool+isAscii w = w < 128++decodeAsciiList :: [Word8] -> Maybe String+decodeAsciiList = mapM+  (\w -> if isAscii w then Just (chr (fromIntegral w)) else Nothing)++textDecodeAscii :: Property+textDecodeAscii = property $ do+  bytes <- forAll genMostlyAsciiBytes+  front <- forAll (genOffset (L.length bytes))+  back <- forAll (genOffset (L.length bytes))+  let truncatedBytes = listDropEnd back (L.drop front bytes)+      expected = decodeAsciiList truncatedBytes+      actual = fmap T.unpack (T.decodeAscii (B.dropEnd back (B.drop front (B.pack bytes))))+  expected === actual++textDecodeUtf8Iso :: Property+textDecodeUtf8Iso = property $ do+  chars <- forAll genString+  front <- forAll (genOffset (L.length chars))+  back <- forAll (genOffset (L.length chars))+  let text = T.dropEnd back (T.drop front (T.pack chars))+      decoded = T.decodeUtf8 (T.encodeUtf8 text)+  Just (T.unpack text) === fmap T.unpack decoded++textDecodeUtf8Surrogates :: Property+textDecodeUtf8Surrogates = property $ do+  chars <- forAll genStringSurrogates+  front <- forAll (genOffset (L.length chars))+  back <- forAll (genOffset (L.length chars))+  let truncatedChars = listDropEnd back (L.drop front chars)+      cleanChars = map+        (\c -> if Data.Char.ord c >= 0xD800 && Data.Char.ord c < 0xE000 then chr 0xFFFD else c)+        truncatedChars+      bytes = B.pack (foldMap charToBytesWithSurrogates truncatedChars)+      text = T.dropEnd back (T.drop front (T.pack chars))+      decoded = T.decodeUtf8 (T.encodeUtf8 text)+  Just cleanChars === fmap T.unpack decoded+  Just (T.unpack text) === fmap T.unpack decoded+  Just cleanChars === fmap T.unpack (T.decodeUtf8 bytes)++textPackProp :: Property+textPackProp = property $ do+  chars <- forAll genString+  front <- forAll (genOffset (L.length chars))+  back <- forAll (genOffset (L.length chars))+  let expected = listDropEnd back (L.drop front chars)+      actual = T.unpack (T.dropEnd back (T.drop front (T.pack chars)))+  expected === actual++textTakeProp :: Property+textTakeProp = property $ do+  chars <- forAll genString+  n <- forAll (genChop (L.length chars))+  let expected = L.take n chars+      actual = T.unpack (T.take n (T.pack chars))+  expected === actual++textDropProp :: Property+textDropProp = property $ do+  chars <- forAll genString+  n <- forAll (genChop (L.length chars))+  let expected = L.drop n chars+      actual = T.unpack (T.drop n (T.pack chars))+  expected === actual+++textBreakCharProp :: Property+textBreakCharProp = property $ do+  chars <- forAll genString+  front <- forAll (genOffset (L.length chars))+  back <- forAll (genOffset (L.length chars))+  let truncatedChars = listDropEnd back (L.drop front chars)+  c <- forAll (pickChar truncatedChars)+  let expected = L.break (== c) truncatedChars+      actual = bimap T.unpack T.unpack (T.breakChar c (T.dropEnd back (T.drop front (T.pack chars))))+  expected === actual++textToUpperProp :: Property+textToUpperProp = property $ do+  chars <- forAll genString+  front <- forAll (genOffset (L.length chars))+  back <- forAll (genOffset (L.length chars))+  let expected = L.map Data.Char.toUpper (listDropEnd back (L.drop front chars))+      actual = T.unpack (T.toUpper (T.dropEnd back (T.drop front (T.pack chars))))+  expected === actual++listDropEnd :: Int -> [a] -> [a]+listDropEnd n xs = L.take (L.length xs - n) xs++pickChar :: String -> Gen Char+pickChar s = if L.null s+  then genCharUnicode+  else element s++pickByte :: [Word8] -> Gen Word8+pickByte s = if L.null s+  then genByte+  else element s++genChop :: Int -> Gen Int+genChop originalLen = integral (linear 0 maxDiscard)+  where+  maxDiscard = div (originalLen * 6) 5++genOffset :: Int -> Gen Int+genOffset originalLen = integral (linear 0 maxDiscard)+  where+  maxDiscard = min 19 (div originalLen 3)++-- Generates a string that is either entirely ascii+-- or that is a healthy mixture of characters with+-- variable UTF-8 byte lengths.+genString :: Gen String+genString = frequency [ (3, genStringAscii), (7, genStringUnicode) ]++genSurrogate :: Gen Char+genSurrogate = fmap chr (int (linear 0xD800 0xDFFF))++genSurrogates :: Gen String+genSurrogates = list (linear 0 3) genSurrogate++-- Generates a string that may contain unicode characters+-- in the range U+D800 to U+DFFF.+genStringSurrogates :: Gen String+genStringSurrogates = choice+  [ apcat [genSurrogates, genStringAscii, genSurrogates]+  , apcat [genSurrogates, genStringUnicode, genSurrogates]+  ]++-- Only uses ascii characters+genStringAscii :: Gen String+genStringAscii = list (linear 0 128) (fmap chr (int (linear 0x00 0x7F)))++-- Pulls from unicode characters that have all different+-- UTF-8 byte-lengths. +genStringUnicode :: Gen String+genStringUnicode = list (linear 0 128) genCharUnicode++genCharUnicode :: Gen Char+genCharUnicode = choice+  [ fmap chr (int (linear 0x00 0x7F))+  , fmap chr (int (linear 0x80 0x7FF))+  , fmap (chr . (\x -> if x >= 0xD800 && x <= 0xDFFF then 0xD799 else x)) (int (linear 0x800 0xFFFF))+  , fmap chr (int (linear 0x10000 0x10FFFF))+  ]++genByte :: Gen Word8+genByte = word8 (linear minBound maxBound)++genMostlyAsciiBytes :: Gen [Word8]+genMostlyAsciiBytes = choice+  [ apcat+    [ list (linear 0 20) (word8 (linear 0x00 0x7F))+    , fmap pure (word8 (linear 0xF0 0xFF))+    , list (linear 0 20) (word8 (linear 0x00 0x7F))+    ]+  , list (linear 0 45) (word8 (linear 0x00 0x7F))+  ]++++findByteProp :: Property+findByteProp = property $ do+  wordList :: [Word8] <- forAll (list (linear 0 128) enumBounded)+  let len = L.length wordList+  mindex <- forAll $ frequency+    [ (4, fmap Just (int (linear 0 (len - 1))))+    , (1, pure Nothing)+    ]+  w <- case mindex of+    Just ix -> case safeIndex ix wordList of+      Just b -> pure b+      Nothing -> if len == 0 && ix == 0 then pure 0 else failure+    Nothing -> case findUnusedByte (S.fromList wordList) of+      Just b -> pure b+      Nothing -> failure+  L.elemIndex w wordList === BA.findByte w (BA.pack wordList)++sliceFindByteProp :: Property+sliceFindByteProp = property $ do+  byteList <- forAll (list (linear 0 128) genByte)+  front <- forAll (genOffset (L.length byteList))+  back <- forAll (genOffset (L.length byteList))+  let truncatedByteList = listDropEnd back (L.drop front byteList)+  w <- forAll (pickByte truncatedByteList)+  let expected = L.elemIndex w truncatedByteList+      actual = B.findByte w (B.dropEnd back (B.drop front (B.pack byteList)))+  expected === actual+++zipAndProp :: Property+zipAndProp = property $ do+  xsList :: [Word8] <- forAll (list (linear 0 128) enumBounded)+  ysList :: [Word8] <- forAll (list (linear 0 128) enumBounded)+  let xs = BA.pack xsList+      ys = BA.pack ysList+  L.zipWith (.&.) xsList ysList === BA.unpack (BA.zipAnd xs ys)++safeIndex :: Int -> [a] -> Maybe a+safeIndex !_ [] = Nothing+safeIndex !ix (x : xs) = case compare ix 0 of+  EQ -> Just x+  LT -> Nothing+  GT -> safeIndex (ix - 1) xs++findUnusedByte :: Set Word8 -> Maybe Word8+findUnusedByte s = S.lookupMin (S.difference allBytes s)++allBytes :: Set Word8+allBytes = S.fromList (enumFromTo minBound maxBound)++apcat :: (Applicative f, Monoid a) => [f a] -> f a+apcat = fmap mconcat . sequenceA++-- This is nearly the same this as Packed.Text.pack. However, it does+-- not replace surrogates with U+FFFD. This is useful for testing+-- that we handle surrogates correctly when decoding UTF-8 text.+charToBytesWithSurrogates :: Char -> [Word8]+charToBytesWithSurrogates c+  | p < 0x80 = [wordToWord8 p]+  | p < 0x800 = [wordToWord8 (byteTwoOne p), wordToWord8 (byteTwoTwo p)]+  | p < 0x10000 = [wordToWord8 (byteThreeOne p), wordToWord8 (byteThreeTwo p), wordToWord8 (byteThreeThree p)]+  | otherwise = [wordToWord8 (byteFourOne p), wordToWord8 (byteFourTwo p), wordToWord8 (byteFourThree p), wordToWord8 (byteFourFour p)]+  where+  p :: Word+  p = fromIntegral (Data.Char.ord c)+  +wordToWord8 :: Word -> Word8+wordToWord8 = fromIntegral++-- precondition: codepoint is less than 0x800+byteTwoOne :: Word -> Word+byteTwoOne w = unsafeShiftR w 6 .|. 0b11000000++byteTwoTwo :: Word -> Word+byteTwoTwo w = (w .&. 0b00111111) .|. 0b10000000++-- precondition: codepoint is less than 0x1000+byteThreeOne :: Word -> Word+byteThreeOne w = unsafeShiftR w 12 .|. 0b11100000++byteThreeTwo :: Word -> Word+byteThreeTwo w = (0b00111111 .&. unsafeShiftR w 6) .|. 0b10000000++byteThreeThree :: Word -> Word+byteThreeThree w = (w .&. 0b00111111) .|. 0b10000000++-- precondition: codepoint is less than 0x110000+byteFourOne :: Word -> Word+byteFourOne w = unsafeShiftR w 18 .|. 0b11110000++byteFourTwo :: Word -> Word+byteFourTwo w = (0b00111111 .&. unsafeShiftR w 12) .|. 0b10000000++byteFourThree :: Word -> Word+byteFourThree w = (0b00111111 .&. unsafeShiftR w 6) .|. 0b10000000++byteFourFour :: Word -> Word+byteFourFour w = (0b00111111 .&. w) .|. 0b10000000+