packages feed

bytebuild 0.3.4.0 → 0.3.5.0

raw patch · 6 files changed

+294/−33 lines, 6 filesdep +integer-logarithmsdep +quickcheck-instancesdep ~primitivePVP ok

version bump matches the API change (PVP)

Dependencies added: integer-logarithms, quickcheck-instances

Dependency ranges changed: primitive

API changes (from Hackage documentation)

+ Data.Bytes.Builder: intLEB128 :: Int -> Builder
+ Data.Bytes.Builder: integerDec :: Integer -> Builder
+ Data.Bytes.Builder: naturalDec :: Natural -> Builder
+ Data.Bytes.Builder: word64LEB128 :: Word64 -> Builder
+ Data.Bytes.Builder: wordLEB128 :: Word -> Builder
+ Data.Bytes.Builder.Bounded: word48PaddedLowerHex :: Word64 -> Builder 12
+ Data.Bytes.Builder.Bounded: word64LEB128 :: Word64 -> Builder 10
+ Data.Bytes.Builder.Bounded: wordLEB128 :: Word -> Builder 10
+ Data.Bytes.Builder.Unsafe: closeBuilderState :: BuilderState s -> Commits s
+ Data.Bytes.Builder.Unsafe: commitDistance :: MutableByteArray# s -> Int# -> Commits s -> Int#
+ Data.Bytes.Builder.Unsafe: commitDistance1 :: MutableByteArray# s -> Int# -> MutableByteArray# s -> Int# -> Commits s -> Int#
+ Data.Bytes.Builder.Unsafe: newBuilderState :: Int -> ST s (BuilderState s)

Files

CHANGELOG.md view
@@ -5,6 +5,14 @@ `small-bytearray-builder` is now just a compatibility shim to ease the migration process. +## 0.3.5.0 -- 2020-05-01++* Add `wordLEB128` and `word64LEB128`.+* Add `integerDec` and `naturalDec`.+* Add `word48PaddedLowerHex`.+* Add `newBuilderState`, `closeBuilderState`, `commitDistance`, and+  `commitDistance1` to the unsafe module.+ ## 0.3.4.0 -- 2020-02-27  * Rename the library from `small-bytearray-builder` to `bytebuild`, and
bytebuild.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: bytebuild-version: 0.3.4.0+version: 0.3.5.0 synopsis: Serialize to a small byte arrays description:   This is similar to the builder facilities provided by@@ -46,6 +46,7 @@     , base >=4.12.0.0 && <5     , byteslice >=0.2 && <0.3     , bytestring >=0.10.8.2 && <0.11+    , integer-logarithms >=1.0.3 && <1.1     , natural-arithmetic >=0.1 && <0.2     , primitive-offset >=0.2 && <0.3     , primitive-unlifted >=0.1.2 && <0.2@@ -79,6 +80,7 @@     , primitive     , primitive-unlifted >=0.1.2     , quickcheck-classes >=0.6.4+    , quickcheck-instances >=0.3.22     , tasty >=1.2.3 && <1.3     , tasty-hunit >=0.10.0.2 && <0.11     , tasty-quickcheck >=0.10.1 && <0.11
src/Data/Bytes/Builder.hs view
@@ -3,8 +3,10 @@ {-# language DuplicateRecordFields #-} {-# language LambdaCase #-} {-# language MagicHash #-}+{-# language NumericUnderscores #-} {-# language RankNTypes #-} {-# language ScopedTypeVariables #-}+{-# language TypeApplications #-} {-# language UnboxedTuples #-}  module Data.Bytes.Builder@@ -36,11 +38,13 @@   , word16Dec   , word8Dec   , wordDec+  , naturalDec   , int64Dec   , int32Dec   , int16Dec   , int8Dec   , intDec+  , integerDec     -- * Unsigned Words     -- ** 64-bit   , word64PaddedUpperHex@@ -82,6 +86,10 @@   , int64LE   , int32LE   , int16LE+    -- **** LEB128+  , intLEB128+  , wordLEB128+  , word64LEB128     -- *** Many   , word8Array     -- **** Big Endian@@ -117,12 +125,13 @@ import Control.Exception (SomeException,toException) import Control.Monad.ST (ST,runST) import Control.Monad.IO.Class (MonadIO,liftIO)-import Data.Bytes.Builder.Unsafe (Builder(Builder))+import Data.Bits (unsafeShiftR,unsafeShiftL,xor,finiteBitSize)+import Data.Bytes.Builder.Unsafe (Builder(Builder),commitDistance1) import Data.Bytes.Builder.Unsafe (BuilderState(BuilderState),pasteIO) import Data.Bytes.Builder.Unsafe (Commits(Initial,Mutable,Immutable)) import Data.Bytes.Builder.Unsafe (reverseCommitsOntoChunks) import Data.Bytes.Builder.Unsafe (commitsOntoChunks)-import Data.Bytes.Builder.Unsafe (stringUtf8,cstring)+import Data.Bytes.Builder.Unsafe (stringUtf8,cstring,fromEffect) import Data.Bytes.Builder.Unsafe (addCommitsLength,copyReverseCommits) import Data.ByteString.Short.Internal (ShortByteString(SBS)) import Data.Bytes.Chunks (Chunks(ChunksNil))@@ -137,9 +146,14 @@ import Foreign.C.String (CStringLen) import GHC.ByteOrder (ByteOrder(BigEndian,LittleEndian),targetByteOrder) import GHC.Exts (Int(I#),Char(C#),Int#,State#,ByteArray#,(>=#))-import GHC.Exts (RealWorld,MutableByteArray#,(+#),(-#),(<#))+import GHC.Exts (RealWorld,(+#),(-#),(<#))+import GHC.Exts ((*#))+import GHC.Integer.Logarithms.Compat (integerLog2#) import GHC.IO (IO(IO),stToIO)+import GHC.Natural (naturalFromInteger,naturalToInteger) import GHC.ST (ST(ST))+import GHC.Word (Word(W#),Word8(W8#))+import Numeric.Natural (Natural)  import qualified Arithmetic.Nat as Nat import qualified Arithmetic.Types as Arithmetic@@ -909,7 +923,6 @@ word16LE :: Word16 -> Builder word16LE w = fromBounded Nat.constant (Bounded.word16LE w) - -- | Requires exactly 32 bytes. Dump the octets of a 256-bit -- word in a big-endian fashion. word256BE :: Word256 -> Builder@@ -991,29 +1004,6 @@ consLength64BE :: Builder -> Builder consLength64BE = consLength Nat.constant (\x -> Bounded.word64BE (fromIntegral x)) --- Internal. This is like commitDistance, but you get to supply a--- head of the commit list that has not yet been committed.-commitDistance1 ::-     MutableByteArray# s -- target-  -> Int# -- offset into target-  -> MutableByteArray# s -- head of array-  -> Int# -- offset into head of array-  -> Commits s-  -> Int#-commitDistance1 target offTarget buf0 offBuf cs =-  case Exts.sameMutableByteArray# target buf0 of-    1# -> offBuf -# offTarget-    _ -> commitDistance target offBuf cs -# offTarget--commitDistance :: MutableByteArray# s -> Int# -> Commits s -> Int#-commitDistance _ !_ Initial = error "chunkDistance: chunk not found"-commitDistance target !n (Immutable _ _ len cs) =-  commitDistance target (n +# len) cs-commitDistance target !n (Mutable buf len cs) =-  case Exts.sameMutableByteArray# target buf of-    1# -> n +# len-    _ -> commitDistance target (n +# len) cs- -- | Push the buffer currently being filled onto the chunk list, -- allocating a new active buffer of the requested size. This is -- helpful when a small builder is sandwhiched between two large@@ -1043,3 +1033,119 @@  c2w :: Char -> Word8 c2w = fromIntegral . ord++-- In C, this is: (n << 1) ^ (n >> (BIT_WIDTH - 1))+zigZagNative :: Int -> Word+zigZagNative s = fromIntegral @Int @Word+  ((unsafeShiftL s 1) `xor` (unsafeShiftR s (finiteBitSize (undefined :: Word) - 1)))++-- | Encode a signed machine-sized integer with LEB-128. This uses+-- zig-zag encoding.+intLEB128 :: Int -> Builder+intLEB128 = wordLEB128 . zigZagNative++-- | Encode a machine-sized word with LEB-128.+wordLEB128 :: Word -> Builder+wordLEB128 w = fromBounded Nat.constant (Bounded.wordLEB128 w)++-- | Encode a 64-bit word with LEB-128.+word64LEB128 :: Word64 -> Builder+word64LEB128 w = fromBounded Nat.constant (Bounded.word64LEB128 w)++-- | Encode a signed arbitrary-precision integer as decimal.+-- This encoding never starts with a zero unless the argument was zero.+-- Negative numbers are preceded by a minus sign. Positive numbers+-- are not preceded by anything.+integerDec :: Integer -> Builder+integerDec !i+  | i < 0 = ascii '-' <> naturalDec (naturalFromInteger (negate i))+  | otherwise = naturalDec (naturalFromInteger i)++-- | Encodes an unsigned arbitrary-precision integer as decimal.+-- This encoding never starts with a zero unless the argument was zero.+naturalDec :: Natural -> Builder+naturalDec !n0 = fromEffect+  (I# (11# +# (3# *# integerLog2# (naturalToInteger n0))))+  (\marr off -> case n0 of+    0 -> do+      PM.writeByteArray marr off (0x30 :: Word8)+      pure (off + 1)+    _ -> go n0 marr off off+  )+  where+  go :: forall s. Natural -> MutableByteArray s -> Int -> Int -> ST s Int+  go !n !buf !off0 !off = case quotRem n 1_000_000_000 of+    (q,r) -> case q of+      0 -> do+        off' <- backwardsWordLoop buf off (fromIntegral @Natural @Word r)+        reverseBytes buf off0 (off' - 1)+        pure off'+      _ -> do+        off' <- backwardsPasteWordPaddedDec9+          (fromIntegral @Natural @Word r) buf off+        go q buf off0 off'++-- Reverse the bytes in the designated slice. This takes+-- an inclusive start offset and an inclusive end offset.+reverseBytes :: MutableByteArray s -> Int -> Int -> ST s ()+{-# inline reverseBytes #-}+reverseBytes arr begin end = go begin end where+  go ixA ixB = if ixA < ixB+    then do+      a :: Word8 <- PM.readByteArray arr ixA+      b :: Word8 <- PM.readByteArray arr ixB+      PM.writeByteArray arr ixA b+      PM.writeByteArray arr ixB a+      go (ixA + 1) (ixB - 1)+    else pure ()++backwardsPasteWordPaddedDec9 ::+  Word -> MutableByteArray s -> Int -> ST s Int+backwardsPasteWordPaddedDec9 !w !arr !off = do+  backwardsPutRem10+    (backwardsPutRem10 $ backwardsPutRem10 $ backwardsPutRem10 $+     backwardsPutRem10 $ backwardsPutRem10 $ backwardsPutRem10 $+     backwardsPutRem10 $ backwardsPutRem10+     (\_ _ _ -> pure ())+    ) arr off w+  pure (off + 9)++backwardsPutRem10 ::+     (MutableByteArray s -> Int -> Word -> ST s a)+  -> MutableByteArray s -> Int -> Word -> ST s a+{-# inline backwardsPutRem10 #-}+backwardsPutRem10 andThen arr off dividend = do+  let quotient = approxDiv10 dividend+      remainder = dividend - (10 * quotient)+  PM.writeByteArray arr off (unsafeWordToWord8 (remainder + 48))+  andThen arr (off + 1) quotient++backwardsWordLoop :: MutableByteArray s -> Int -> Word -> ST s Int+{-# inline backwardsWordLoop #-}+backwardsWordLoop arr off0 x0 = go off0 x0 where+  go !off !(x :: Word) = if x > 0+    then do+      let (y,z) = quotRem x 10+      PM.writeByteArray arr off (fromIntegral (z + 0x30) :: Word8)+      go (off + 1) y+    else pure off++-- Based on C code from https://stackoverflow.com/a/5558614+-- For numbers less than 1073741829, this gives a correct answer.+approxDiv10 :: Word -> Word+approxDiv10 !n = unsafeShiftR (0x1999999A * n) 32+  +-- -- A weird beast useful for rewrite rules. Not yet used. This will+-- -- ultimately replace fromEffect and fromBounded.+-- require :: Int -> Builder+-- require !n = Builder $ \buf0 off0 len0 cs0 s0 ->+--   let !(I# req) = n+--    in case len0 >=# req of+--         1# -> (# s0, buf0, off0, len0, cs0 #)+--         _ -> let !(I# lenX) = max 4080 (I# req) in+--           case Exts.newByteArray# lenX s0 of+--             (# sX, bufX #) ->+--               (# sX, bufX, 0#, lenX, Mutable buf0 off0 cs0 #)++unsafeWordToWord8 :: Word -> Word8+unsafeWordToWord8 (W# w) = W8# w
src/Data/Bytes/Builder/Bounded.hs view
@@ -45,6 +45,8 @@     -- ** 64-bit   , word64PaddedLowerHex   , word64PaddedUpperHex+    -- ** 48-bit+  , word48PaddedLowerHex     -- ** 32-bit   , word32PaddedLowerHex   , word32PaddedUpperHex@@ -89,6 +91,9 @@   , int64LE   , int32LE   , int16LE+    -- **** LEB128+  , wordLEB128+  , word64LEB128     -- * Encode Floating-Point Types   , doubleDec   ) where@@ -161,7 +166,10 @@  -- | Concatenate two builders. append :: Builder m -> Builder n -> Builder (m + n)-append (Builder f) (Builder g) =+append = unsafeAppend++unsafeAppend :: Builder m -> Builder n -> Builder p+unsafeAppend (Builder f) (Builder g) =   Builder $ \arr off0 s0 -> case f arr off0 s0 of     (# s1, r #) -> g arr r s1 @@ -286,15 +294,20 @@  internalWordLoop :: MutableByteArray s -> Int -> Word -> ST s Int {-# inline internalWordLoop #-}-internalWordLoop arr off0 x0 = go off0 x0 where+internalWordLoop arr off0 x0 = do+  off1 <- backwardsWordLoop arr off0 x0+  reverseBytes arr off0 (off1 - 1)+  pure off1++backwardsWordLoop :: MutableByteArray s -> Int -> Word -> ST s Int+{-# inline backwardsWordLoop #-}+backwardsWordLoop arr off0 x0 = go off0 x0 where   go !off !(x :: Word) = if x > 0     then do       let (y,z) = quotRem x 10       writeByteArray arr off (fromIntegral (z + 0x30) :: Word8)       go (off + 1) y-    else do-      reverseBytes arr off0 (off - 1)-      pure off+    else pure off  -- Requires up to 20 bytes. Can be less depending on what the -- size of the argument is known to be. Unsafe.@@ -390,6 +403,14 @@ word64PaddedLowerHex :: Word64 -> Builder 16 word64PaddedLowerHex (W64# w) = word64PaddedLowerHex# w +-- | Requires exactly 12 bytes. Discards the upper 16 bits of a+-- 64-bit unsigned integer and then encodes the lower 48 bits as+-- hexadecimal, zero-padding the encoding to 12 digits. This uses+-- lowercase for the alphabetical digits. For example, this encodes the+-- number 1022 as @0000000003fe@.+word48PaddedLowerHex :: Word64 -> Builder 12+word48PaddedLowerHex (W64# w) = word48PaddedLowerHex# w+ -- | Requires exactly 8 bytes. Encodes a 32-bit unsigned integer as -- hexadecimal, zero-padding the encoding to 8 digits. This uses -- uppercase for the alphabetical digits.@@ -483,6 +504,27 @@  -- TODO: Is it actually worth unrolling this loop. I suspect that it -- might not be. Benchmark this.+word48PaddedLowerHex# :: Word# -> Builder 12+{-# noinline word48PaddedLowerHex# #-}+word48PaddedLowerHex# w# = Unsafe.construct $ \arr off -> do+  writeByteArray arr off (toHexLower (unsafeShiftR w 44))+  writeByteArray arr (off + 1) (toHexLower (unsafeShiftR w 40))+  writeByteArray arr (off + 2) (toHexLower (unsafeShiftR w 36))+  writeByteArray arr (off + 3) (toHexLower (unsafeShiftR w 32))+  writeByteArray arr (off + 4) (toHexLower (unsafeShiftR w 28))+  writeByteArray arr (off + 5) (toHexLower (unsafeShiftR w 24))+  writeByteArray arr (off + 6) (toHexLower (unsafeShiftR w 20))+  writeByteArray arr (off + 7) (toHexLower (unsafeShiftR w 16))+  writeByteArray arr (off + 8) (toHexLower (unsafeShiftR w 12))+  writeByteArray arr (off + 9) (toHexLower (unsafeShiftR w 8))+  writeByteArray arr (off + 10) (toHexLower (unsafeShiftR w 4))+  writeByteArray arr (off + 11) (toHexLower w)+  pure (off + 12)+  where+  w = W# w#++-- TODO: Is it actually worth unrolling this loop. I suspect that it+-- might not be. Benchmark this. word64PaddedLowerHex# :: Word# -> Builder 16 {-# noinline word64PaddedLowerHex# #-} word64PaddedLowerHex# w# = Unsafe.construct $ \arr off -> do@@ -752,6 +794,22 @@   primitive_ (writeCharArray# arr (off +# 5# ) c5)   pure (I# (off +# 6# )) +-- | Encode a machine-sized word with LEB-128.+wordLEB128 :: Word -> Builder 10+wordLEB128 (W# w) = lebCommon (W# w)++-- | Encode a 64-bit word with LEB-128.+word64LEB128 :: Word64 -> Builder 10+word64LEB128 (W64# w) = lebCommon (W# w)++lebCommon :: Word -> Builder n+lebCommon !w = case quotRem w 128 of+  (q,r) -> case q of+    0 -> unsafeWord8 (unsafeWordToWord8 r)+    _ -> unsafeAppend+      (unsafeWord8 (unsafeWordToWord8 (r .|. 0x80)))+      (lebCommon q)+ -- | Encode a character as UTF-8. This only uses as much space as is required. char :: Char -> Builder 4 char c@@ -910,6 +968,11 @@  word8 :: Word8 -> Builder 1 word8 w = Unsafe.construct $ \arr off -> do+  writeByteArray arr off w+  pure (off + 1)++unsafeWord8 :: Word8 -> Builder n+unsafeWord8 w = Unsafe.construct $ \arr off -> do   writeByteArray arr off w   pure (off + 1) 
src/Data/Bytes/Builder/Unsafe.hs view
@@ -16,11 +16,17 @@   , pasteIO     -- * Construction   , fromEffect+    -- * Builder State+  , newBuilderState+  , closeBuilderState     -- * Finalization   , reverseCommitsOntoChunks   , commitsOntoChunks   , copyReverseCommits   , addCommitsLength+    -- * Commit Distance+  , commitDistance+  , commitDistance1     -- * Safe Functions     -- | These functions are actually completely safe, but they are defined     -- here because they are used by typeclass instances. Import them from@@ -58,12 +64,29 @@       (# State# s, MutableByteArray# s, Int#, Int#, Commits s #) -- all the same things     ) +-- | A list of committed chunks along with the chunk currently being+-- written to. This is kind of like a non-empty variant of 'Commmits'+-- but with the additional invariant that the head chunk is a mutable+-- byte array. data BuilderState s = BuilderState   (MutableByteArray# s) -- buffer we are currently writing to   Int# -- offset into the current buffer   Int# -- number of bytes remaining in the current buffer   !(Commits s) -- buffers and immutable byte slices that are already committed +-- | Create an empty 'BuilderState' with a buffer of the given size.+newBuilderState :: Int -> ST s (BuilderState s)+{-# inline newBuilderState #-}+newBuilderState n@(I# n# ) = do+  MutableByteArray buf <- PM.newByteArray n+  pure (BuilderState buf 0# n# Initial)++-- | Push the active chunk onto the top of the commits.+-- The @BuilderState@ argument must not be reused after being passed+-- to this function. That is, its use must be affine.+closeBuilderState :: BuilderState s -> Commits s+closeBuilderState (BuilderState dst off _ cmts) = Mutable dst off cmts+ -- | Run a builder, performing an in-place update on the state. -- The @BuilderState@ argument must not be reused after being passed -- to this function. That is, its use must be affine.@@ -258,3 +281,30 @@ shrinkMutableByteArray :: MutableByteArray s -> Int -> ST s () shrinkMutableByteArray (MutableByteArray arr) (I# sz) =   primitive_ (Exts.shrinkMutableByteArray# arr sz)++-- | Variant of commitDistance where you get to supply a+-- head of the commit list that has not yet been committed.+commitDistance1 ::+     MutableByteArray# s -- target+  -> Int# -- offset into target+  -> MutableByteArray# s -- head of array+  -> Int# -- offset into head of array+  -> Commits s+  -> Int#+commitDistance1 target offTarget buf0 offBuf cs =+  case Exts.sameMutableByteArray# target buf0 of+    1# -> offBuf -# offTarget+    _ -> commitDistance target offBuf cs -# offTarget++-- | Compute the number of bytes between the last byte and the offset+-- specified in a chunk. Precondition: the chunk must exist in the+-- list of committed chunks. This relies on mutable byte arrays having+-- identity (e.g. it uses @sameMutableByteArray#@).+commitDistance :: MutableByteArray# s -> Int# -> Commits s -> Int#+commitDistance !_ !_ Initial = errorWithoutStackTrace "chunkDistance: chunk not found"+commitDistance target !n (Immutable _ _ len cs) =+  commitDistance target (n +# len) cs+commitDistance target !n (Mutable buf len cs) =+  case Exts.sameMutableByteArray# target buf of+    1# -> n +# len+    _ -> commitDistance target (n +# len) cs
test/Main.hs view
@@ -15,13 +15,17 @@ import Data.IORef (IORef,newIORef,readIORef,writeIORef) import Data.Primitive (ByteArray) import Data.WideWord (Word128(Word128),Word256(Word256))+import Numeric.Natural (Natural) import Test.Tasty (defaultMain,testGroup,TestTree) import Test.QuickCheck ((===),Arbitrary)+import Test.QuickCheck.Instances.Natural () import Text.Printf (printf) import Test.Tasty.HUnit ((@=?))  import qualified Arithmetic.Nat as Nat+import qualified Data.Bits as Bits import qualified Data.Bytes.Builder.Bounded as Bounded+import qualified Data.Bytes as Bytes import qualified Data.ByteString as ByteString import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Lazy.Char8 as LB@@ -215,6 +219,19 @@          in runConcat 1 (foldMap word256BE xs)             ===             runConcat 1 (word256ArrayBE ys 0 (Prelude.length xs))+    , TQC.testProperty "word64LEB128" $ \(x :: Word64) ->+        runConcat 1 (word64LEB128 x)+        ===+        naiveLeb128 (fromIntegral x)+    , TQC.testProperty "naturalDec-A" $ \(x :: Natural) ->+        runConcat 1 (naturalDec x)+        ===+        pack (show x)+    , TQC.testProperty "naturalDec-B" $ \(x :: Natural) ->+        let y = 1234567892345678934678987654321 * x in+        runConcat 1 (naturalDec y)+        ===+        pack (show y)     ]   , testGroup "alternate"     [ TQC.testProperty "HexWord64" $ \x y ->@@ -306,3 +323,18 @@ zeroPadL n s   | length s < n = replicate (n - length s) '0' ++ s   | otherwise = s++naiveLeb128 :: Natural -> ByteArray+naiveLeb128 x =+  Bytes.toByteArray (Bytes.unsafeDrop 1 (Exts.fromList (0xFF : go [] x)))+  where+  go !xs !n =+    let (q,r) = quotRem n 128+        r' = fromIntegral @Natural @Word8 r+        w = if q == 0+          then r'+          else Bits.setBit r' 7+        xs' = w : xs+     in if q == 0 +          then L.reverse xs'+          else go xs' q