text-builder-linear 0.1.1.1 → 0.1.2
raw patch · 12 files changed
+957/−146 lines, 12 filesdep ~tasty
Dependency ranges changed: tasty
Files
- bench/BenchChar.hs +171/−4
- bench/BenchHexadecimal.hs +19/−11
- changelog.md +11/−0
- src/Data/Text/Builder/Linear/Array.hs +86/−0
- src/Data/Text/Builder/Linear/Buffer.hs +68/−15
- src/Data/Text/Builder/Linear/Char.hs +151/−5
- src/Data/Text/Builder/Linear/Core.hs +56/−19
- src/Data/Text/Builder/Linear/Dec.hs +11/−6
- src/Data/Text/Builder/Linear/Double.hs +1/−1
- src/Data/Text/Builder/Linear/Hex.hs +51/−8
- test/Main.hs +323/−74
- text-builder-linear.cabal +9/−3
bench/BenchChar.hs view
@@ -10,8 +10,12 @@ import Data.Char import qualified Data.Text as T import Data.Text.Builder.Linear.Buffer+import qualified Data.Text.Lazy as TL import Data.Text.Lazy (toStrict)+import qualified Data.Text.Lazy.Builder as TB import Data.Text.Lazy.Builder (toLazyText, singleton)+import qualified Data.Text.Internal.Fusion.Common as Fusion+import qualified Data.Text.Internal.Fusion as Fusion import Test.Tasty.Bench #ifdef MIN_VERSION_text_builder@@ -22,6 +26,10 @@ import qualified ByteString.StrictBuilder #endif +--------------------------------------------------------------------------------+-- Single char+--------------------------------------------------------------------------------+ benchLazyBuilder ∷ Int → T.Text benchLazyBuilder = toStrict . toLazyText . go mempty where@@ -57,11 +65,11 @@ go !acc 0 = acc go !acc n = let ch = chr n in go (ch .<| (acc |>. ch)) (n - 1) -benchChar ∷ Benchmark-benchChar = bgroup "Char" $ map mkGroup [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6]+benchSingleChar ∷ Benchmark+benchSingleChar = bgroup "Single" $ map mkGroupChar [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6] -mkGroup :: Int → Benchmark-mkGroup n = bgroup (show n)+mkGroupChar :: Int → Benchmark+mkGroupChar n = bgroup (show n) [ bench "Data.Text.Lazy.Builder" $ nf benchLazyBuilder n , bench "Data.ByteString.Builder" $ nf benchLazyBuilderBS n #ifdef MIN_VERSION_text_builder@@ -72,3 +80,162 @@ #endif , bench "Data.Text.Builder.Linear" $ nf benchLinearBuilder n ]++--------------------------------------------------------------------------------+-- Multiple chars+--------------------------------------------------------------------------------++charCount :: Word+charCount = 3++benchCharsLazyBuilder ∷ Int → T.Text+benchCharsLazyBuilder = TL.toStrict . TB.toLazyText . go mempty+ where+ go !acc 0 = acc+ go !acc n = let ch = chr n in go (replicateChar ch <> (acc <> replicateChar ch)) (n - 1)++ replicateChar ch = TB.fromText (Fusion.unstream (Fusion.replicateCharI charCount ch))++{- [FIXME] bad performance+benchCharsLazyBuilderBS ∷ Int → B.ByteString+benchCharsLazyBuilderBS = B.toStrict . B.toLazyByteString . go mempty+ where+ go !acc 0 = acc+ go !acc n =+ let ch = chr n+ in go (replicateChar ch <> (acc <> replicateChar ch)) (n - 1)++ replicateChar ch = stimes charCount (B.charUtf8 ch)+-}++#ifdef MIN_VERSION_text_builder+benchCharsStrictBuilder ∷ Int → T.Text+benchCharsStrictBuilder = Text.Builder.run . go mempty+ where+ go !acc 0 = acc+ go !acc n = let ch = chr n in go (replicateChar ch <> (acc <> replicateChar ch)) (n - 1)++ -- [TODO] Is there a better way?+ replicateChar ch = Text.Builder.padFromRight (fromIntegral charCount) ch mempty+#endif++{- [TODO]+#ifdef MIN_VERSION_bytestring_strict_builder+benchCharsStrictBuilderBS ∷ Int → B.ByteString+benchCharsStrictBuilderBS = ByteString.StrictBuilder.builderBytes . go mempty+ where+ go !acc 0 = acc+ go !acc n = let ch = chr n in go _ (n - 1)+#endif+-}++benchCharsLinearBuilder ∷ Int → T.Text+benchCharsLinearBuilder m = runBuffer (\b → go b m)+ where+ go ∷ Buffer ⊸ Int → Buffer+ go !acc 0 = acc+ go !acc n = let ch = chr n in go (prependChars charCount ch (appendChars charCount ch acc)) (n - 1)++benchMultipleChars ∷ Benchmark+benchMultipleChars = bgroup "Multiple" $ map mkGroupChars [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6]++mkGroupChars :: Int → Benchmark+mkGroupChars n = bgroup (show n)+ [ bench "Data.Text.Lazy.Builder" $ nf benchCharsLazyBuilder n+ -- , bench "Data.ByteString.Builder" $ nf benchCharsLazyBuilderBS n+#ifdef MIN_VERSION_text_builder+ , bench "Text.Builder" $ nf benchCharsStrictBuilder n+#endif+-- #ifdef MIN_VERSION_bytestring_strict_builder+-- , bench "ByteString.StrictBuilder" $ nf benchCharsStrictBuilderBS n+-- #endif+ , bench "Data.Text.Builder.Linear" $ nf benchCharsLinearBuilder n+ ]++--------------------------------------------------------------------------------+-- Padding+--------------------------------------------------------------------------------++benchPaddingLazyBuilder ∷ Int → T.Text+benchPaddingLazyBuilder = toStrict . toLazyText . go mempty 0+ where+ go !acc !_ 0 = acc+ go !acc l n =+ let ch = chr n+ !l' = l + 2 * fromIntegral charCount+ in go (withText (T.justifyLeft l' ch)+ (withText (T.justifyRight (l + fromIntegral charCount) ch) acc))+ l'+ (n - 1)++ withText f = TB.fromText . f . TL.toStrict . TB.toLazyText++{- [TODO]+benchPaddingLazyBuilderBS ∷ Int → B.ByteString+benchPaddingLazyBuilderBS = B.toStrict . B.toLazyByteString . go mempty+ where+ go !acc 0 = acc+ go !acc n = let ch = chr n in go _ (n - 1)+-}++#ifdef MIN_VERSION_text_builder+benchPaddingStrictBuilder ∷ Int → T.Text+benchPaddingStrictBuilder = Text.Builder.run . go mempty 0+ where+ go !acc !_ 0 = acc+ go !acc l n =+ let ch = chr n+ !l' = l + 2 * fromIntegral charCount+ in go (Text.Builder.padFromRight l' ch (Text.Builder.padFromLeft (l + fromIntegral charCount) ch acc))+ l'+ (n - 1)+#endif++{- [TODO]+#ifdef MIN_VERSION_bytestring_strict_builder+benchPaddingStrictBuilderBS ∷ Int → B.ByteString+benchPaddingStrictBuilderBS = ByteString.StrictBuilder.builderBytes . go mempty+ where+ go !acc 0 = acc+ go !acc n = let ch = chr n in go _ (n - 1)+#endif+-}++benchPaddingLinearBuilder ∷ Int → T.Text+benchPaddingLinearBuilder m = runBuffer (\b → go b 0 m)+ where+ go ∷ Buffer ⊸ Word → Int → Buffer+ go !acc !_ 0 = acc+ go !acc l n =+ let ch = chr n+ !l' = l + 2 * charCount+ in go (justifyLeft l' ch (justifyRight (l + charCount) ch acc))+ l'+ (n - 1)++benchPadding ∷ Benchmark+benchPadding = bgroup "Padding" $ map mkGroupPadding [1e0, 1e1, 1e2, 1e3, 1e4{-, 1e5, 1e6-}] -- NOTE: too long with 1e5++mkGroupPadding :: Int → Benchmark+mkGroupPadding n = bgroup (show n)+ [ bench "Data.Text.Lazy.Builder" $ nf benchPaddingLazyBuilder n+ -- , bench "Data.ByteString.Builder" $ nf benchPaddingLazyBuilderBS n+#ifdef MIN_VERSION_text_builder+ , bench "Text.Builder" $ nf benchPaddingStrictBuilder n+#endif+-- #ifdef MIN_VERSION_bytestring_strict_builder+-- , bench "ByteString.StrictBuilder" $ nf benchPaddingStrictBuilderBS n+-- #endif+ , bench "Data.Text.Builder.Linear" $ nf benchPaddingLinearBuilder n+ ]++--------------------------------------------------------------------------------+-- All benchmarks+--------------------------------------------------------------------------------++benchChar ∷ Benchmark+benchChar = bgroup "Char"+ [ benchSingleChar+ , benchMultipleChars+ , benchPadding ]+
bench/BenchHexadecimal.hs view
@@ -21,42 +21,50 @@ word :: Word word = 123456789123456789 -benchLazyBuilder ∷ Int → T.Text+benchLazyBuilder ∷ Word → T.Text benchLazyBuilder = toStrict . toLazyText . go mempty where go !acc 0 = acc- go !acc n = let i = fromIntegral n * word in go (hexadecimal i <> (acc <> hexadecimal i)) (n - 1)+ go !acc n = let i = n * word in go (hexadecimal i <> (acc <> hexadecimal i)) (n - 1) -benchLazyBuilderBS ∷ Int → B.ByteString+benchLazyBuilderBS ∷ Word → B.ByteString benchLazyBuilderBS = B.toStrict . B.toLazyByteString . go mempty where go !acc 0 = acc- go !acc n = go (B.wordHex (fromIntegral n) <> (acc <> B.wordHex (fromIntegral n))) (n - 1)+ go !acc n = go (B.wordHex n <> (acc <> B.wordHex n)) (n - 1) #ifdef MIN_VERSION_text_builder-benchStrictBuilder ∷ Int → T.Text+benchStrictBuilder ∷ Word → T.Text benchStrictBuilder = Text.Builder.run . go mempty where go !acc 0 = acc- go !acc n = let i = fromIntegral n * word in go (Text.Builder.hexadecimal i <> (acc <> Text.Builder.hexadecimal i)) (n - 1)+ go !acc n = let i = n * word in go (Text.Builder.hexadecimal i <> (acc <> Text.Builder.hexadecimal i)) (n - 1) #endif -benchLinearBuilder ∷ Int → T.Text-benchLinearBuilder m = runBuffer (\b → go b m)+benchLinearBuilderWord ∷ Word → T.Text+benchLinearBuilderWord m = runBuffer (\b → go b m) where+ go ∷ Buffer ⊸ Word → Buffer+ go !acc 0 = acc+ go !acc n = let i = n * word in go (i &<| (acc |>& i)) (n - 1)++benchLinearBuilderInt ∷ Word → T.Text+benchLinearBuilderInt m = runBuffer (\b → go b (fromIntegral m))+ where go ∷ Buffer ⊸ Int → Buffer go !acc 0 = acc- go !acc n = let i = fromIntegral n * word in go (i &<| (acc |>& i)) (n - 1)+ go !acc n = let i = n * fromIntegral word in go (i &<| (acc |>& i)) (n - 1) benchHexadecimal ∷ Benchmark benchHexadecimal = bgroup "Hexadecimal" $ map mkGroup [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6] -mkGroup :: Int → Benchmark+mkGroup :: Word → Benchmark mkGroup n = bgroup (show n) [ bench "Data.Text.Lazy.Builder" $ nf benchLazyBuilder n , bench "Data.ByteString.Builder" $ nf benchLazyBuilderBS n #ifdef MIN_VERSION_text_builder , bench "Text.Builder" $ nf benchStrictBuilder n #endif- , bench "Data.Text.Builder.Linear" $ nf benchLinearBuilder n+ , bench "Data.Text.Builder.Linear (Word)" $ nf benchLinearBuilderWord n+ , bench "Data.Text.Builder.Linear (Int)" $ nf benchLinearBuilderInt n ]
changelog.md view
@@ -1,3 +1,14 @@+## 0.1.2++* Fix unsound behaviour caused by inlining of `runBuffer` / `runBufferBS`+ and CSE (common subexpression elimination).+* Fix hexadecimal builder, looping on negative inputs.+* Fix decimal builder for non-standard bitness of the input.+* Add `(#<|)` and deprecate `(|>#)`.+* Add `newEmptyBuffer`.+* Add `prependChars` and `appendChars`.+* Add `justifyLeft`, `justifyRight` and `center`.+ ## 0.1.1.1 * Support `text-2.1`.
+ src/Data/Text/Builder/Linear/Array.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE CPP #-}++-- |+-- Copyright: (c) 2022 Andrew Lelechenko+-- (c) 2023 Pierre Le Marre+-- Licence: BSD3+-- Maintainer: Andrew Lelechenko <andrew.lelechenko@gmail.com>+--+-- Low-level routines for 'A.MArray' manipulations.+module Data.Text.Builder.Linear.Array (+ unsafeThaw,+ sizeofByteArray,+ isPinned,+ unsafeTile,+ unsafeReplicate,+) where++import Data.Text.Array qualified as A+import GHC.Exts (Int (..), isByteArrayPinned#, isTrue#, setByteArray#, sizeofByteArray#)+import GHC.ST (ST (..))++#if __GLASGOW_HASKELL__ >= 909+import GHC.Exts (unsafeThawByteArray#)+#else+import GHC.Exts (unsafeCoerce#)+#endif++unsafeThaw ∷ A.Array → ST s (A.MArray s)+#if __GLASGOW_HASKELL__ >= 909+unsafeThaw (A.ByteArray a) = ST $ \s# → case unsafeThawByteArray# a s# of+ (# s'#, ma #) -> (# s'#, A.MutableByteArray ma #)+#else+unsafeThaw (A.ByteArray a) = ST $ \s# →+ (# s#, A.MutableByteArray (unsafeCoerce# a) #)+#endif++sizeofByteArray ∷ A.Array → Int+sizeofByteArray (A.ByteArray a) = I# (sizeofByteArray# a)++isPinned ∷ A.Array → Bool+isPinned (A.ByteArray a) = isTrue# (isByteArrayPinned# a)++-- | Replicate an ASCII character+--+-- __Warning:__ it is the responsibility of the caller to ensure that the 'Int'+-- is a valid ASCII character.+unsafeReplicate+ ∷ A.MArray s+ -- ^ Mutable array+ → Int+ -- ^ Offset+ → Int+ -- ^ Count+ → Int+ -- ^ ASCII character+ → ST s ()+unsafeReplicate (A.MutableByteArray dst#) (I# dstOff#) (I# count#) (I# w#) =+ ST (\s# → (# setByteArray# dst# dstOff# count# w# s#, () #))+{-# INLINE unsafeReplicate #-}++-- | Duplicate a portion of an array in-place.+--+-- Example of use:+--+-- @+-- -- Write @count@ times the char @c@+-- let cLen = utf8Length c; totalLen = cLen * count+-- in unsafeWrite dst dstOff ch *> 'unsafeTile' dst dstOff totalLen cLen+-- @+unsafeTile+ ∷ A.MArray s+ -- ^ Mutable array+ → Int+ -- ^ Start of the portion to duplicate+ → Int+ -- ^ Total length of the duplicate+ → Int+ -- ^ Length of the portion to duplicate+ → ST s ()+unsafeTile dest destOff totalLen = go+ where+ -- Adapted from Data.Text.Array.tile+ go l+ | 2 * l > totalLen = A.copyM dest (destOff + l) dest destOff (totalLen - l)+ | otherwise = A.copyM dest (destOff + l) dest destOff l *> go (2 * l)+{-# INLINE unsafeTile #-}
src/Data/Text/Builder/Linear/Buffer.hs view
@@ -1,32 +1,68 @@ -- | -- Copyright: (c) 2022 Andrew Lelechenko+-- (c) 2023 Pierre Le Marre -- Licence: BSD3 -- Maintainer: Andrew Lelechenko <andrew.lelechenko@gmail.com> -- -- 'Buffer' for strict 'Text', based on linear types. module Data.Text.Builder.Linear.Buffer (+ -- * Type Buffer,++ -- * Basic interface runBuffer, runBufferBS, dupBuffer, consumeBuffer, eraseBuffer, foldlIntoBuffer,- (|>),+ newEmptyBuffer,+ (><),++ -- * Single character (|>.),- (|>#),- (<|), (.<|),++ -- * Multiple characters++ -- ** Character replication+ prependChars,+ appendChars,++ -- ** Text+ (|>),+ (<|),+ (|>…),+ (…<|),++ -- ** Raw 'Addr#'+ (|>#),+ ( #<| ), -- NOTE: extra spaces required because of -XUnboxedTuples (<|#),- (><),++ -- * Padding+ justifyLeft,+ justifyRight,+ center,++ -- * Number formatting++ -- ** Decimal (|>$), ($<|),- (|>%),- (%<|),++ -- ** Hexadecimal++ -- *** Lower-case (|>&), (&<|),- (|>…),- (…<|),++ -- *** Upper-case and padding+ -- $custom_hexadecimal++ -- ** Double+ (|>%),+ (%<|), ) where import Data.Text.Array qualified as A@@ -56,7 +92,7 @@ buffer -- | Prepend 'Text' prefix to a 'Buffer' by mutating it.--- If a prefix is statically known, consider using '(<|#)' for optimal performance.+-- If a prefix is statically known, consider using '(#<|)' for optimal performance. -- -- >>> :set -XOverloadedStrings -XLinearTypes -- >>> runBuffer (\b -> "foo" <| "bar" <| b)@@ -79,8 +115,6 @@ -- -- The literal string must not contain zero bytes @\\0@ and must be a valid UTF-8, -- these conditions are not checked.------ Note the inconsistency in naming: unfortunately, GHC parser does not allow for @#<|@. (|>#) ∷ Buffer ⊸ Addr# → Buffer infixl 6 |>#@@ -96,15 +130,21 @@ -- to a 'Buffer' by mutating it. E. g., -- -- >>> :set -XOverloadedStrings -XLinearTypes -XMagicHash--- >>> runBuffer (\b -> "foo"# <|# "bar"# <|# b)+-- >>> runBuffer (\b -> "foo"# #<| "bar"# #<| b) -- "foobar" -- -- The literal string must not contain zero bytes @\\0@ and must be a valid UTF-8, -- these conditions are not checked.-(<|#) ∷ Addr# → Buffer ⊸ Buffer+--+-- /Note:/ When the syntactic extensions @UnboxedTuples@ or @UnboxedSums@ are+-- enabled, extra spaces are required when using parentheses: i.e. use @( '#<|' )@+-- instead of @('#<|')@. See the GHC User Guide chapter+-- “[Unboxed types and primitive operations](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/primitives.html#unboxed-tuples)”+-- for further information.+( #<| ) ∷ Addr# → Buffer ⊸ Buffer -infixr 6 <|#-addr# <|# buffer =+infixr 6 #<|, <|#+addr# #<| buffer = prependExact srcLen (\dst dstOff → A.copyFromPointer dst dstOff (Ptr addr#) srcLen)@@ -112,6 +152,12 @@ where srcLen = I# (cstringLength# addr#) +-- | Alias for @'(#<|)'@.+{-# DEPRECATED (<|#) "Use '(#<|)' instead" #-}+(<|#) ∷ Addr# → Buffer ⊸ Buffer+(<|#) = ( #<| ) -- NOTE: extra spaces required because of -XUnboxedTuples+{-# INLINE (<|#) #-}+ -- | Append given number of spaces. (|>…) ∷ Buffer ⊸ Word → Buffer @@ -152,3 +198,10 @@ go ∷ Buffer ⊸ [a] → Buffer go !acc [] = acc go !acc (x : xs) = go (f acc x) xs++-- $custom_hexadecimal+--+-- Note that no /upper/ case hexadecimal formatting is provided. This package+-- provides a minimal API with utility functions only for common cases. For+-- other use cases, please adapt the code of this package, e.g. as shown in+-- the [Unicode code point example](https://github.com/Bodigrim/linear-builder/examples/src/Examples/Unicode.hs).
src/Data/Text/Builder/Linear/Char.hs view
@@ -3,26 +3,43 @@ -- Licence: BSD3 -- Maintainer: Andrew Lelechenko <andrew.lelechenko@gmail.com> module Data.Text.Builder.Linear.Char (- -- * Buffer+ -- * Single character (|>.), (.<|),++ -- * Multiple characters+ prependChars,+ appendChars,++ -- * Padding+ justifyLeft,+ justifyRight,+ center, ) where +import Data.Char (isAscii) import Data.Text.Array qualified as A import Data.Text.Internal.Encoding.Utf8 (ord2, ord3, ord4, utf8Length) import Data.Text.Internal.Unsafe.Char (ord, unsafeWrite) import GHC.ST (ST)+import Unsafe.Coerce (unsafeCoerce) +import Data.Text.Builder.Linear.Array (unsafeReplicate, unsafeTile) import Data.Text.Builder.Linear.Core +--------------------------------------------------------------------------------+-- Single char+--------------------------------------------------------------------------------+ -- | Append 'Char' to a 'Buffer' by mutating it. -- -- >>> :set -XLinearTypes -- >>> runBuffer (\b -> b |>. 'q' |>. 'w') -- "qw" ----- In contrast to 'Data.Text.Lazy.Builder.singleton', it's a responsibility--- of the caller to sanitize surrogate code points with 'Data.Text.Internal.safe'.+-- __Warning:__ In contrast to 'Data.Text.Lazy.Builder.singleton', it is the+-- responsibility of the caller to sanitize surrogate code points with+-- 'Data.Text.Internal.safe'. (|>.) ∷ Buffer ⊸ Char → Buffer infixl 6 |>.@@ -34,8 +51,9 @@ -- >>> runBuffer (\b -> 'q' .<| 'w' .<| b) -- "qw" ----- In contrast to 'Data.Text.Lazy.Builder.singleton', it's a responsibility--- of the caller to sanitize surrogate code points with 'Data.Text.Internal.safe'.+-- __Warning:__ In contrast to 'Data.Text.Lazy.Builder.singleton', it is the+-- responsibility of the caller to sanitize surrogate code points with+-- 'Data.Text.Internal.safe'. (.<|) ∷ Char → Buffer ⊸ Buffer infixr 6 .<|@@ -72,3 +90,131 @@ A.unsafeWrite marr (off - 2) n2 A.unsafeWrite marr (off - 1) n3 pure 4++--------------------------------------------------------------------------------+-- Multiple chars+--------------------------------------------------------------------------------++-- | Prepend a given count of a 'Char' to a 'Buffer'.+--+-- >>> :set -XLinearTypes+-- >>> runBuffer (\b -> prependChars 3 'x' (b |>. 'A'))+-- "xxxA"+prependChars ∷ Word → Char → Buffer ⊸ Buffer+prependChars count ch buff+ | count == 0 = buff+ | otherwise =+ case utf8Length ch of+ cLen → case cLen * fromIntegral count of+ totalLen →+ prependExact+ totalLen+ ( if isAscii ch+ then \dst dstOff → unsafeReplicate dst dstOff (fromIntegral count) (ord ch)+ else \dst dstOff → unsafeWrite dst dstOff ch *> unsafeTile dst dstOff totalLen cLen+ )+ buff++-- | Apppend a given count of a 'Char' to a 'Buffer'.+--+-- >>> :set -XLinearTypes+-- >>> runBuffer (\b -> appendChars 3 'x' (b |>. 'A'))+-- "Axxx"+appendChars ∷ Word → Char → Buffer ⊸ Buffer+appendChars count ch buff+ | count == 0 = buff+ | otherwise =+ case utf8Length ch of+ cLen → case cLen * fromIntegral count of+ totalLen →+ appendExact+ totalLen+ ( if isAscii ch+ then \dst dstOff → unsafeReplicate dst dstOff (fromIntegral count) (ord ch)+ else \dst dstOff → unsafeWrite dst dstOff ch *> unsafeTile dst dstOff totalLen cLen+ )+ buff++--------------------------------------------------------------------------------+-- Padding+--------------------------------------------------------------------------------++-- | Pad a builder from the /left/ side to the specified length with the specified+-- character.+--+-- >>> :set -XLinearTypes+-- >>> runBuffer (\b -> justifyRight 10 'x' (appendChars 3 'A' b))+-- "xxxxxxxAAA"+-- >>> runBuffer (\b -> justifyRight 5 'x' (appendChars 6 'A' b))+-- "AAAAAA"+--+-- Note that 'newEmptyBuffer' is needed in some situations. The following example creates+-- a utility function that justify a text and then append it to a buffer.+--+-- >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples+-- >>> import Data.Text.Builder.Linear.Buffer+-- >>> import Data.Text (Text)+-- >>> :{+-- appendJustified :: Buffer %1 -> Text -> Buffer+-- appendJustified b t = case newEmptyBuffer b of+-- -- Note that we need to create a new buffer from the text, in order+-- -- to justify only the text and not the input buffer.+-- (# b', empty #) -> b' >< justifyRight 12 ' ' (empty |> t)+-- :}+--+-- >>> runBuffer (\b -> (b |> "Test:") `appendJustified` "foo" `appendJustified` "bar")+-- "Test: foo bar"+justifyRight ∷ Word → Char → Buffer ⊸ Buffer+justifyRight n ch buff = case lengthOfBuffer buff of+ (# buff', len #) →+ toLinearWord+ (\l b → if n <= l then b else prependChars (n - l) ch b)+ len+ buff'++-- | Pad a builder from the /right/ side to the specified length with the specified+-- character.+--+-- >>> :set -XLinearTypes+-- >>> runBuffer (\b -> justifyLeft 10 'x' (appendChars 3 'A' b))+-- "AAAxxxxxxx"+-- >>> runBuffer (\b -> justifyLeft 5 'x' (appendChars 6 'A' b))+-- "AAAAAA"+--+-- Note that 'newEmptyBuffer' is needed in some situations. See 'justifyRight'+-- for an example.+justifyLeft ∷ Word → Char → Buffer ⊸ Buffer+justifyLeft n ch buff = case lengthOfBuffer buff of+ (# buff', len #) →+ toLinearWord+ (\l b → if n <= l then b else appendChars (n - l) ch b)+ len+ buff'++-- | Center a builder to the specified length with the specified character.+--+-- >>> :set -XLinearTypes+-- >>> runBuffer (\b -> center 10 'x' (appendChars 3 'A' b))+-- "xxxxAAAxxx"+-- >>> runBuffer (\b -> center 5 'x' (appendChars 6 'A' b))+-- "AAAAAA"+--+-- Note that 'newEmptyBuffer' is needed in some situations. See 'justifyRight'+-- for an example.+center ∷ Word → Char → Buffer ⊸ Buffer+center n ch buff = case lengthOfBuffer buff of+ (# buff', len #) →+ toLinearWord+ ( \l b →+ if n <= l+ then b+ else case n - l of+ !d → case d `quot` 2 of+ !r → appendChars r ch (prependChars (d - r) ch b)+ )+ len+ buff'++-- Despite the use of unsafeCoerce, this is safe.+toLinearWord ∷ (Word → a) → (Word ⊸ a)+toLinearWord = unsafeCoerce
src/Data/Text/Builder/Linear/Core.hs view
@@ -1,11 +1,15 @@ -- | -- Copyright: (c) 2022 Andrew Lelechenko+-- (c) 2023 Pierre Le Marre -- Licence: BSD3 -- Maintainer: Andrew Lelechenko <andrew.lelechenko@gmail.com> -- -- Low-level routines for 'Buffer' manipulations. module Data.Text.Builder.Linear.Core (+ -- * Type Buffer,++ -- * Basic interface runBuffer, runBufferBS, dupBuffer,@@ -15,6 +19,9 @@ lengthOfBuffer, dropBuffer, takeBuffer,+ newEmptyBuffer,++ -- * Text concatenation appendBounded, appendExact, prependBounded,@@ -26,10 +33,12 @@ import Data.Text qualified as T import Data.Text.Array qualified as A import Data.Text.Internal (Text (..))-import GHC.Exts (Int (..), Levity (..), RuntimeRep (..), TYPE, byteArrayContents#, isByteArrayPinned#, isTrue#, plusAddr#, sizeofByteArray#, unsafeCoerce#)+import GHC.Exts (Int (..), Levity (..), RuntimeRep (..), TYPE, byteArrayContents#, plusAddr#, unsafeCoerce#) import GHC.ForeignPtr (ForeignPtr (..), ForeignPtrContents (..)) import GHC.ST (ST (..), runST) +import Data.Text.Builder.Linear.Array+ -- | Internally 'Buffer' is a mutable buffer. -- If a client gets hold of a variable of type 'Buffer', -- they'd be able to pass a mutable buffer to concurrent threads.@@ -66,10 +75,10 @@ -- | Run a linear function on an empty 'Buffer', producing a strict 'Text'. ----- Be careful to write @runBuffer (\b -> ...)@ instead of @runBuffer $ \b -> ...@,+-- Be careful to write @runBuffer (\\b -> ...)@ instead of @runBuffer $ \\b -> ...@, -- because current implementation of linear types lacks special support for '($)'. -- Another option is to enable @{-# LANGUAGE BlockArguments #-}@--- and write @runBuffer \b -> ...@.+-- and write @runBuffer \\b -> ...@. -- Alternatively, you can import -- [@($)@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#v:-36-) -- from [@linear-base@](https://hackage.haskell.org/package/linear-base).@@ -83,7 +92,16 @@ -- 'Text' and [@Ur@](https://hackage.haskell.org/package/linear-base-0.3.0/docs/Prelude-Linear.html#t:Ur) 'Text' are equivalent. runBuffer ∷ (Buffer ⊸ Buffer) ⊸ Text runBuffer f = unBuffer (shrinkBuffer (f (Buffer mempty)))+{-# NOINLINE runBuffer #-} +{-+ See https://github.com/Bodigrim/linear-builder/issues/19+ and https://github.com/tweag/linear-base/pull/187#discussion_r489081926+ for the discussion why NOINLINE here and below in 'runBufferBS' is necessary.+ Without it CSE (common subexpression elimination) can pull out 'Buffer's from+ different 'runBuffer's and share them, which is absolutely not what we want.+-}+ -- | Same as 'runBuffer', but returning a UTF-8 encoded strict 'ByteString'. runBufferBS ∷ (Buffer ⊸ Buffer) ⊸ ByteString runBufferBS f = case shrinkBuffer (f (Buffer memptyPinned)) of@@ -91,6 +109,7 @@ where addr# = byteArrayContents# arr `plusAddr#` from fp = ForeignPtr addr# (PlainPtr (unsafeCoerce# arr))+{-# NOINLINE runBufferBS #-} shrinkBuffer ∷ Buffer ⊸ Buffer shrinkBuffer (Buffer (Text arr from len)) = Buffer $ runST $ do@@ -105,6 +124,34 @@ arr ← A.unsafeFreeze marr pure $ Text arr 0 0 +-- | Create an empty 'Buffer'.+--+-- The first 'Buffer' is the input and the second is a new empty 'Buffer'.+--+-- This function is needed in some situations, e.g. with+-- 'Data.Text.Builder.Linear.Buffer.justifyRight'. The following example creates+-- a utility function that justify a text and then append it to a buffer.+--+-- >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples+-- >>> import Data.Text.Builder.Linear.Buffer+-- >>> import Data.Text (Text)+-- >>> :{+-- appendJustified :: Buffer %1 -> Text -> Buffer+-- appendJustified b t = case newEmptyBuffer b of+-- -- Note that we need to create a new buffer from the text, in order+-- -- to justify only the text and not the input buffer.+-- (# b', empty #) -> b' >< justifyRight 12 ' ' (empty |> t)+-- :}+--+-- >>> runBuffer (\b -> (b |> "Test:") `appendJustified` "foo" `appendJustified` "bar")+-- "Test: foo bar"+--+-- Note: a previous buffer is necessary in order to create an empty buffer with+-- the same characteristics.+newEmptyBuffer ∷ Buffer ⊸ (# Buffer, Buffer #)+newEmptyBuffer (Buffer t@(Text arr _ _)) =+ (# Buffer t, Buffer (if isPinned arr then memptyPinned else mempty) #)+ -- | Duplicate builder. Feel free to process results in parallel threads. -- Similar to -- [@Dupable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Dupable)@@ -120,7 +167,7 @@ -- -- >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples -- >>> import Data.Text.Builder.Linear.Buffer--- >>> runBuffer (\b -> (\(# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar")) (dupBuffer b))+-- >>> runBuffer (\b -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar")) -- "foobar" -- -- Note the unboxed tuple: 'Buffer' is an unlifted datatype,@@ -152,9 +199,9 @@ -- import Data.Unrestricted.Linear -- -- dropEndBuffer :: Word -> Buffer %1 -> Buffer--- dropEndBuffer n buf =--- (\(# buf', len #) -> case move len of Ur len' -> takeBuffer (len' - n) buf')--- (lengthOfBuffer buf)+-- dropEndBuffer n buf = case lengthOfBuffer buf of+-- (# buf', len #) -> case move len of+-- Ur len' -> takeBuffer (len' - n) buf' -- @ lengthOfBuffer ∷ Buffer ⊸ (# Buffer, Word #) lengthOfBuffer (Buffer t) = (# Buffer t, fromIntegral (T.length t) #)@@ -242,7 +289,7 @@ pure $ Text new newOff (dstLen + srcLen) {-# INLINE prependBounded #-} --- | Low-level routine to append data of unknown size to a 'Buffer'.+-- | Low-level routine to append data of known size to a 'Buffer'. prependExact ∷ Int -- ^ Exact number of bytes, written by an action@@ -257,23 +304,13 @@ (\dst dstOff → appender dst dstOff >> pure srcLen) {-# INLINE prependExact #-} -unsafeThaw ∷ A.Array → ST s (A.MArray s)-unsafeThaw (A.ByteArray a) = ST $ \s# →- (# s#, A.MutableByteArray (unsafeCoerce# a) #)--sizeofByteArray ∷ A.Array → Int-sizeofByteArray (A.ByteArray a) = I# (sizeofByteArray# a)--isPinned ∷ A.Array → Bool-isPinned (A.ByteArray a) = isTrue# (isByteArrayPinned# a)- -- | Concatenate two 'Buffer's, potentially mutating both of them. -- -- You likely need to use 'dupBuffer' to get hold on two builders at once: -- -- >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples -- >>> import Data.Text.Builder.Linear.Buffer--- >>> runBuffer (\b -> (\(# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar")) (dupBuffer b))+-- >>> runBuffer (\b -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar")) -- "foobar" (><) ∷ Buffer ⊸ Buffer ⊸ Buffer
src/Data/Text/Builder/Linear/Dec.hs view
@@ -66,12 +66,12 @@ where go ∷ (Integral a, FiniteBits a) ⇒ Int → a → Int go acc k- | finiteBitSize k >= 30, k >= 1000000000 = go (acc + 9) (quotBillion k)+ | finiteBitSize k >= if isSigned k then 31 else 30, k >= 1e9 = go (acc + 9) (quotBillion k) | otherwise = acc + goInt (fromIntegral k) goInt l@(I# l#)- | l >= 1e5 = 5 + I# (l# >=# 100000000#) + I# (l# >=# 10000000#) + I# (l# >=# 1000000#)- | otherwise = I# (l# >=# 10000#) + I# (l# >=# 1000#) + I# (l# >=# 100#) + I# (l# >=# 10#)+ | l >= 1e5 = 5 + I# (l# >=# 100_000_000#) + I# (l# >=# 10_000_000#) + I# (l# >=# 1_000_000#)+ | otherwise = I# (l# >=# 10_000#) + I# (l# >=# 1_000#) + I# (l# >=# 100#) + I# (l# >=# 10#) {-# INLINEABLE exactDecLen #-} unsafeAppendDec ∷ (Integral a, FiniteBits a) ⇒ A.MArray s → Int → a → ST s Int@@ -82,7 +82,7 @@ unsafePrependDec marr !off n | n < 0 , n == bit (finiteBitSize n - 1) = do- A.unsafeWrite marr (off - 1) (fromIntegral (48 + minBoundLastDigit n))+ A.unsafeWrite marr (off - 1) (fromIntegral (0x30 + minBoundLastDigit n)) go (off - 2) (abs (bit (finiteBitSize n - 1) `quot` 10)) >>= sign | n == 0 = do A.unsafeWrite marr (off - 1) 0x30 >> pure 1@@ -101,15 +101,20 @@ A.copyFromPointer marr (o - 1) (Ptr digits `plusPtr` (fromIntegral r `shiftL` 1)) 2 if k < 100 then pure (o - 1) else go (o - 2) q | otherwise = do- A.unsafeWrite marr o (fromIntegral (48 + k))+ A.unsafeWrite marr o (fromIntegral (0x30 + k)) pure o digits ∷ Addr# digits = "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899"# {-# INLINEABLE unsafePrependDec #-} +-- Compute rem minBound 10 efficiently. Given that:+-- • minBound = 1 `shiftL` (finiteBitSize a - 1) = -2^(finiteBitSize a - 1)+-- • the last digit of 2^k forms a cycle for k≥1: 2,4,8,6+-- Then it is enough to pattern-match rem (finiteBitSize a) 4,+-- i.e. finiteBitSize a .&. 3 minBoundLastDigit ∷ FiniteBits a ⇒ a → Int-minBoundLastDigit a = case finiteBitSize a .&. 4 of+minBoundLastDigit a = case finiteBitSize a .&. 3 of 0 → 8 1 → 6 2 → 2
src/Data/Text/Builder/Linear/Double.hs view
@@ -29,7 +29,7 @@ (\dst dstOff → unsafeAppendDouble dst dstOff x) buffer --- | Prepend double+-- | Prepend double. (%<|) ∷ Double → Buffer ⊸ Buffer infixr 6 %<|
src/Data/Text/Builder/Linear/Hex.hs view
@@ -9,34 +9,58 @@ import Data.Bits (Bits (..), FiniteBits (..)) import Data.Text.Array qualified as A+import Data.Word (Word16, Word32, Word64, Word8) import GHC.Exts (Int (..), (>#)) import GHC.ST (ST) import Data.Text.Builder.Linear.Core --- | Append hexadecimal number.+-- | Append the lower-case hexadecimal represensation of a number.+--+-- Negative numbers are interpreted as their corresponding unsigned number, e.g.+--+-- >>> :set -XOverloadedStrings -XLinearTypes+-- >>> import Data.Int (Int8, Int16)+-- >>> runBuffer (\b -> b |>& (-1 :: Int8)) == "ff"+-- True+-- >>> runBuffer (\b -> b |>& (-1 :: Int16)) == "ffff"+-- True (|>&) ∷ (Integral a, FiniteBits a) ⇒ Buffer ⊸ a → Buffer infixl 6 |>& buffer |>& n = appendBounded- (finiteBitSize n `shiftR` 2)+ (maxHexLen n) (\dst dstOff → unsafeAppendHex dst dstOff n) buffer {-# INLINEABLE (|>&) #-} --- | Prepend hexadecimal number.+-- | Prepend the lower-case hexadecimal representation of a number.+--+-- Negative numbers are interpreted as their corresponding unsigned number, e.g.+--+-- >>> :set -XOverloadedStrings -XLinearTypes+-- >>> import Data.Int (Int8, Int16)+-- >>> runBuffer (\b -> (-1 :: Int8) &<| b) == "ff"+-- True+-- >>> runBuffer (\b -> (-1 :: Int16) &<| b) == "ffff"+-- True (&<|) ∷ (Integral a, FiniteBits a) ⇒ a → Buffer ⊸ Buffer infixr 6 &<| n &<| buffer = prependBounded- (finiteBitSize n `shiftR` 2)+ (maxHexLen n) (\dst dstOff → unsafePrependHex dst dstOff n) (\dst dstOff → unsafeAppendHex dst dstOff n) buffer {-# INLINEABLE (&<|) #-} +-- | Compute the number of nibbles that an integral type can hold, rounded up.+maxHexLen ∷ (Integral a, FiniteBits a) ⇒ a → Int+maxHexLen n = 1 + ((finiteBitSize n - 1) `shiftR` 2)+{-# INLINEABLE maxHexLen #-}+ unsafeAppendHex ∷ (Integral a, FiniteBits a) ⇒ A.MArray s → Int → a → ST s Int unsafeAppendHex marr !off 0 = A.unsafeWrite marr off 0x30 >> pure 1@@ -48,7 +72,7 @@ go !o m = do let nibble = m .&. 0x0f writeNibbleAsHex marr o (fromIntegral nibble)- go (o - 1) (m `shiftR` 4)+ go (o - 1) (dropNibble m) {-# INLINEABLE unsafeAppendHex #-} unsafePrependHex ∷ (Integral a, FiniteBits a) ⇒ A.MArray s → Int → a → ST s Int@@ -60,12 +84,31 @@ go !o m = do let nibble = m .&. 0x0f writeNibbleAsHex marr o (fromIntegral nibble)- go (o - 1) (m `shiftR` 4)+ go (o - 1) (dropNibble m) {-# INLINEABLE unsafePrependHex #-} --- | This assumes n /= 0.+-- | The usual 'shiftR' performs sign extension on signed number types,+-- filling the top bits with 1 if the argument is negative.+-- We don't want this behaviour here.+--+-- It would suffice to clean the sign bit only once+-- instead of doing it on every iteration of unsafe{Ap,Pre}pernHex.go,+-- but the performance impact is likely negligible.+dropNibble ∷ (Integral a, FiniteBits a) ⇒ a → a+dropNibble x = case (isSigned x, finiteBitSize x) of+ -- This is morally 'iShiftRL#', 'uncheckedIShiftRA64#', etc.,+ -- but there is no polymorphic interface to access them.+ (True, 8) → fromIntegral @Word8 (shiftR (fromIntegral x) 4)+ (True, 16) → fromIntegral @Word16 (shiftR (fromIntegral x) 4)+ (True, 32) → fromIntegral @Word32 (shiftR (fromIntegral x) 4)+ (True, 64) → fromIntegral @Word64 (shiftR (fromIntegral x) 4)+ (True, _) → shiftR x 4 .&. ((1 `shiftL` (finiteBitSize x - 4)) - 1)+ _ → shiftR x 4+{-# INLINE dropNibble #-}++-- | This assumes n /= 0. Round the number of nibbles up, as in 'maxHexLen'. lengthAsHex ∷ FiniteBits a ⇒ a → Int-lengthAsHex n = (finiteBitSize n `shiftR` 2) - (countLeadingZeros n `shiftR` 2)+lengthAsHex n = 1 + shiftR (finiteBitSize n - countLeadingZeros n - 1) 2 {-# INLINEABLE lengthAsHex #-} writeNibbleAsHex ∷ A.MArray s → Int → Int → ST s ()
test/Main.hs view
@@ -1,3 +1,10 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+#if __GLASGOW_HASKELL__ >= 907+{-# LANGUAGE TypeAbstractions #-}+#endif+ -- | -- Copyright: (c) 2022 Andrew Lelechenko -- Licence: BSD3@@ -5,17 +12,25 @@ module Main where -import Data.Bits (Bits(..), FiniteBits(..))-import Data.Foldable+import Prelude hiding (Foldable(..))+import Data.Bits (Bits(..), FiniteBits(..), bitDefault)+import Data.Foldable (Foldable(..))+import Data.Int+import Data.List (intersperse)+import Data.Proxy (Proxy(..)) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Text.Builder.Linear.Buffer import Data.Text.Internal (Text(..)) import Data.Text.Lazy (toStrict)+import Data.Text.Lazy.Builder qualified as TB import Data.Text.Lazy.Builder (toLazyText) import Data.Text.Lazy.Builder.Int (decimal, hexadecimal) import Data.Text.Lazy.Builder.RealFloat (realFloat)+import Data.Word import GHC.Generics+import GHC.TypeLits (KnownNat, OrderingI (..), SomeNat (..), cmpNat, natVal, sameNat, someNatVal)+import Numeric.Natural (Natural) import Test.Tasty import Test.Tasty.QuickCheck hiding ((><), (.&.)) @@ -37,12 +52,25 @@ | PrependText Text | AppendChar Char | PrependChar Char- | AppendHex Word- | PrependHex Word- | AppendDec Int- | PrependDec Int- | AppendDec30 Int30- | PrependDec30 Int30+ | AppendChars Word Char+ | PrependChars Word Char+ | JustifyLeft Word Char+ | JustifyRight Word Char+ | Center Word Char+ | HexInt Int8 Int16 (IntN 30) (IntN 31) Int32 (IntN 33) Int64+ | HexWord Word8 Word16 Word32 Word64+ | AppendHexI SomeIntN+ | PrependHexI SomeIntN+ | AppendHexW SomeWordN+ | PrependHexW SomeWordN+ | DecInt Int8 Int16 (IntN 30) (IntN 31) Int32 (IntN 33) Int64+ | DecWord Word8 Word16 (WordN 30) (WordN 31) Word32 (WordN 33) Word64+ | AppendDecW Word+ | PrependDecW Word+ | AppendDecI Int+ | PrependDecI Int+ | AppendDecI30 (IntN 30)+ | PrependDecI30 (IntN 30) | AppendDouble Double | PrependDouble Double | AppendSpaces Word@@ -55,61 +83,143 @@ , PrependText <$> arbitrary , AppendChar <$> arbitraryUnicodeChar , PrependChar <$> arbitraryUnicodeChar- , AppendHex <$> arbitraryBoundedIntegral- , PrependHex <$> arbitraryBoundedIntegral- , AppendDec <$> arbitraryBoundedIntegral- , PrependDec <$> arbitraryBoundedIntegral- , AppendDec30 <$> arbitraryBoundedIntegral- , PrependDec30 <$> arbitraryBoundedIntegral- , pure $ AppendHex minBound- , pure $ AppendHex maxBound- , pure $ AppendDec minBound- , pure $ AppendDec maxBound- , pure $ AppendDec 0+ , AppendChars <$> arbitraryCharCount <*> arbitraryUnicodeChar+ , PrependChars <$> arbitraryCharCount <*> arbitraryUnicodeChar+ , JustifyLeft <$> arbitraryTotalLength <*> arbitraryUnicodeChar+ , JustifyRight <$> arbitraryTotalLength <*> arbitraryUnicodeChar+ , Center <$> arbitraryTotalLength <*> arbitraryUnicodeChar+ , AppendHexI <$> arbitrary+ , PrependHexI <$> arbitrary+ , AppendHexW <$> arbitrary+ , PrependHexW <$> arbitrary+ , AppendDecW <$> arbitraryBoundedIntegral+ , PrependDecW <$> arbitraryBoundedIntegral+ , AppendDecI <$> arbitraryBoundedIntegral+ , PrependDecI <$> arbitraryBoundedIntegral+ , AppendDecI30 <$> arbitraryBoundedIntegral+ , PrependDecI30 <$> arbitraryBoundedIntegral+ , pure $ HexWord minBound minBound minBound minBound+ , pure $ HexWord maxBound maxBound maxBound maxBound+ , pure $ HexInt minBound minBound minBound minBound minBound minBound minBound+ , pure $ HexInt maxBound maxBound maxBound maxBound maxBound maxBound maxBound+ , pure $ HexInt 0 0 0 0 0 0 0+ , pure $ DecInt minBound minBound minBound minBound minBound minBound minBound+ , pure $ DecInt maxBound maxBound maxBound maxBound maxBound maxBound maxBound+ , pure $ DecInt 0 0 0 0 0 0 0+ , pure $ DecWord minBound minBound minBound minBound minBound minBound minBound+ , pure $ DecWord maxBound maxBound maxBound maxBound maxBound maxBound maxBound , AppendDouble <$> arbitrary , PrependDouble <$> arbitrary , AppendSpaces . getNonNegative <$> arbitrary , PrependSpaces . getNonNegative <$> arbitrary ]+ where+ arbitraryCharCount = chooseBoundedIntegral (0, 6)+ arbitraryTotalLength = chooseBoundedIntegral (3, 20)+ shrink = genericShrink interpretOnText ∷ [Action] → Text → Text interpretOnText xs z = foldl' go z xs where go ∷ Text → Action → Text- go b (AppendText x) = b <> x- go b (PrependText x) = x <> b- go b (AppendChar x) = T.snoc b x- go b (PrependChar x) = T.cons x b- go b (AppendHex x) = b <> toStrict (toLazyText (hexadecimal x))- go b (PrependHex x) = toStrict (toLazyText (hexadecimal x)) <> b- go b (AppendDec x) = b <> toStrict (toLazyText (decimal x))- go b (PrependDec x) = toStrict (toLazyText (decimal x)) <> b- go b (AppendDec30 x) = b <> toStrict (toLazyText (decimal x))- go b (PrependDec30 x) = toStrict (toLazyText (decimal x)) <> b+ go b (AppendText x) = b <> x+ go b (PrependText x) = x <> b+ go b (AppendChar x) = T.snoc b x+ go b (PrependChar x) = T.cons x b+ go b (AppendChars n x) = b <> T.replicate (fromIntegral n) (T.singleton x)+ go b (PrependChars n x) = T.replicate (fromIntegral n) (T.singleton x) <> b+ go b (JustifyLeft n x) = T.justifyLeft (fromIntegral n) x b+ go b (JustifyRight n x) = T.justifyRight (fromIntegral n) x b+ go b (Center n x) = T.center (fromIntegral n) x b+ go b (HexInt r s t u v w x)+ = intersperseText+ [ hexadecimal (fromIntegral @Int16 @Word16 s)+ , hexadecimalI t+ , hexadecimal (fromIntegral @Int64 @Word64 x) ]+ <> b+ <> intersperseText+ [ hexadecimal (fromIntegral @Int8 @Word8 r)+ , hexadecimalI u+ , hexadecimal (fromIntegral @Int32 @Word32 v)+ , hexadecimalI w ]+ go b (HexWord u v w x)+ = intersperseText [hexadecimal u, hexadecimal x]+ <> b+ <> intersperseText [hexadecimal v, hexadecimal w ]+ go b (AppendHexI x) = b <> toStrict (toLazyText (hexadecimalSI x))+ go b (PrependHexI x) = toStrict (toLazyText (hexadecimalSI x)) <> b+ go b (AppendHexW x) = b <> toStrict (toLazyText (hexadecimalSW x))+ go b (PrependHexW x) = toStrict (toLazyText (hexadecimalSW x)) <> b+ go b (DecInt r s t u v w x)+ = intersperseText [decimal s, decimal t, decimal x]+ <> b+ <> intersperseText [decimal r, decimal u, decimal v, decimal w]+ go b (DecWord r s t u v w x)+ = intersperseText [decimal s, decimal t, decimal x]+ <> b+ <> intersperseText [decimal r, decimal u, decimal v, decimal w]+ go b (AppendDecW x) = b <> toStrict (toLazyText (decimal x))+ go b (PrependDecW x) = toStrict (toLazyText (decimal x)) <> b+ go b (AppendDecI x) = b <> toStrict (toLazyText (decimal x))+ go b (PrependDecI x) = toStrict (toLazyText (decimal x)) <> b+ go b (AppendDecI30 x) = b <> toStrict (toLazyText (decimal x))+ go b (PrependDecI30 x) = toStrict (toLazyText (decimal x)) <> b go b (AppendDouble x) = b <> toStrict (toLazyText (realFloat x)) go b (PrependDouble x) = toStrict (toLazyText (realFloat x)) <> b go b (AppendSpaces n) = b <> T.replicate (fromIntegral n) (T.singleton ' ') go b (PrependSpaces n) = T.replicate (fromIntegral n) (T.singleton ' ') <> b + hexadecimalSI (SomeIntN x) = hexadecimalI x++ hexadecimalI ∷ (KnownNat n) ⇒ IntN n → TB.Builder+ hexadecimalI x = if x >= 0+ then hexadecimal x+ else hexadecimal (fromIntegral @_ @Word64 x .&. (shiftL 1 (intSize x) - 1))++ hexadecimalSW (SomeWordN x) = hexadecimalW x++ hexadecimalW ∷ (KnownNat n) ⇒ WordN n → TB.Builder+ hexadecimalW x = if x >= 0+ then hexadecimal x+ else hexadecimal (fromIntegral @_ @Word64 x .&. (shiftL 1 (intSize x) - 1))++ intersperseText ∷ [TB.Builder] → Text+ intersperseText bs =+ toStrict (toLazyText (mconcat (intersperse (TB.singleton ';') bs)))+ interpretOnBuffer ∷ [Action] → Buffer ⊸ Buffer interpretOnBuffer xs z = foldlIntoBuffer go z xs where go ∷ Buffer ⊸ Action → Buffer- go b (AppendText x) = b |> x- go b (PrependText x) = x <| b- go b (AppendChar x) = b |>. x- go b (PrependChar x) = x .<| b- go b (AppendHex x) = b |>& x- go b (PrependHex x) = x &<| b- go b (AppendDec x) = b |>$ x- go b (PrependDec x) = x $<| b- go b (AppendDec30 x) = b |>$ x- go b (PrependDec30 x) = x $<| b- go b (AppendDouble x) = b |>% x- go b (PrependDouble x) = x %<| b- go b (AppendSpaces n) = b |>… n- go b (PrependSpaces n) = n …<| b+ go b (AppendText x) = b |> x+ go b (PrependText x) = x <| b+ go b (AppendChar x) = b |>. x+ go b (PrependChar x) = x .<| b+ go b (AppendChars n x) = appendChars n x b+ go b (PrependChars n x) = prependChars n x b+ go b (JustifyLeft n x) = justifyLeft n x b+ go b (JustifyRight n x) = justifyRight n x b+ go b (Center n x) = center n x b+ go b (HexInt r s t u v w x) = s &<| ";"# #<| t &<| ";"# #<| x &<|+ (b |>& r |># ";"# |>& u |># ";"# |>& v |># ";"# |>& w)+ go b (HexWord u v w x) = u &<| ";"# #<| x &<| (b |>& v |># ";"# |>& w)+ go b (AppendHexI x) = case x of {SomeIntN i → b |>& i}+ go b (PrependHexI x) = case x of {SomeIntN i → i &<| b}+ go b (AppendHexW x) = case x of {SomeWordN i → b |>& i}+ go b (PrependHexW x) = case x of {SomeWordN i → i &<| b}+ go b (DecInt r s t u v w x) = s $<| ";"# #<| t $<| ";"# #<| x $<| (b |>$ r |># ";"# |>$ u |># ";"# |>$ v |># ";"# |>$ w)+ go b (DecWord r s t u v w x) = s $<| ";"# #<| t $<| ";"# #<| x $<| (b |>$ r |># ";"# |>$ u |># ";"# |>$ v |># ";"# |>$ w)+ go b (AppendDecW x) = b |>$ x+ go b (PrependDecW x) = x $<| b+ go b (AppendDecI x) = b |>$ x+ go b (PrependDecI x) = x $<| b+ go b (AppendDecI30 x) = b |>$ x+ go b (PrependDecI30 x) = x $<| b+ go b (AppendDouble x) = b |>% x+ go b (PrependDouble x) = x %<| b+ go b (AppendSpaces n) = b |>… n+ go b (PrependSpaces n) = n …<| b main ∷ IO () main = defaultMain $ testGroup "All"@@ -118,6 +228,8 @@ , testProperty "append addr#" prop3 , testProperty "prepend addr#" prop4 , testProperty "bytestring builder" prop5+ , testProperty "CSE 1" prop6+ , testProperty "CSE 2" prop7 ] prop1 ∷ [Action] → Property@@ -144,53 +256,190 @@ where addr# = "foo"# f1, f2 :: Buffer ⊸ Buffer- f1 = \b → addr# <|# interpretOnBuffer acts b+ f1 = \b → addr# #<| interpretOnBuffer acts b f2 = \b → T.pack "foo" <| interpretOnBuffer acts b prop5 ∷ [Action] → Property prop5 acts = T.encodeUtf8 (interpretOnText acts mempty) === runBufferBS (\b → interpretOnBuffer acts b) --------------------------------------------------------------------------------+prop6 :: Property+prop6 = T.pack "_a_b" ===+ runBuffer (\buf -> buf |>. '_' |>. 'a' |>+ runBuffer (\buf' -> buf' |>. '_' |>. 'b')) -newtype Int30 = Int30' Int- deriving stock (Eq, Ord, Show)+prop7 :: Property+prop7 =+ let !x = runBuffer (\buf -> (buf |>. '_' |>. 'a') |>… 5)+ !y = runBuffer (\buf -> (buf |>. '_' |>. 'b') |>… 5)+ in (x, y) === (T.pack "_a ", T.pack "_b ")++--------------------------------------------------------------------------------+-- IntN+--------------------------------------------------------------------------------++newtype IntN (n ∷ Natural) = IntN' {unIntN ∷ Int64}+ deriving stock (Eq, Ord) deriving newtype (Enum, Real, Integral) -pattern Int30 :: Int -> Int30-pattern Int30 x <- Int30' x where- Int30 x = Int30' (x .&. ((1 `shiftL` 30) - 1))-{-# COMPLETE Int30 #-}+instance (KnownNat n) ⇒ Show (IntN n) where+ showsPrec p (IntN x) = showParen (p > 10)+ (\s → mconcat ["IntN @", show (natVal (Proxy @n)), " ", show x, s]) -instance Arbitrary Int30 where- arbitrary = Int30 <$> arbitrary- shrink (Int30 x) = Int30 <$> shrink x+pattern IntN ∷ forall n. (KnownNat n) => Int64 → IntN n+pattern IntN x ← IntN' x where+ IntN x = IntN' x'+ where+ -- If the nth bit is 1, then interpret the value as negative and fill the+ -- bits from nth position with 1s. Otherwise clear them to 0s.+ size = intSize (Proxy @n)+ x' = if testBit x (size - 1)+ then x .|. m1+ else x .&. m2+ m1 = complement ((1 `shiftL` (size - 1)) - 1)+ m2 = (1 `shiftL` size) - 1 -instance Bounded Int30 where- minBound = negate (1 `shiftL` 30)- maxBound = (1 `shiftL` 30) - 1+{-# COMPLETE IntN #-} -instance Num Int30 where- Int30 x + Int30 y = Int30 (x + y)- Int30 x * Int30 y = Int30 (x * y)- abs (Int30 x) = Int30 (abs x)+intSize ∷ forall p n. (KnownNat n) => p n → Int+intSize _ = fromInteger (natVal (Proxy @n))++instance (KnownNat n) => Arbitrary (IntN n) where+ arbitrary =+ IntN <$> chooseBoundedIntegral (unIntN @n minBound, unIntN @n maxBound)+ shrink = shrinkIntegral++instance (KnownNat n) => Bounded (IntN n) where+ minBound = IntN (negate (1 `shiftL` (intSize (Proxy @n) - 1)))+ maxBound = IntN ((1 `shiftL` (intSize (Proxy @n) - 1)) - 1)++instance (KnownNat n) => Num (IntN n) where+ IntN x + IntN y = IntN (x + y)+ IntN x * IntN y = IntN (x * y)+ abs (IntN x) = IntN (abs x) signum = undefined- negate (Int30 x) = Int30 (negate x)- fromInteger x = Int30 (fromInteger x)+ negate (IntN x) = IntN (negate x)+ fromInteger x = IntN (fromInteger x) -instance Bits Int30 where- (.&.) = undefined- (.|.) = undefined+instance (KnownNat n) => Bits (IntN n) where+ IntN a .&. IntN b = IntN (a .&. b)+ IntN a .|. IntN b = IntN (a .|. b) xor = undefined- complement = undefined- shift (Int30 x) i = Int30 (shift x i)+ complement (IntN x) = IntN (complement x)+ shift (IntN x) i = IntN (shift x i) rotate = undefined- bitSize = const 30- bitSizeMaybe = const (Just 30)+ bitSize = const (intSize (Proxy @n))+ bitSizeMaybe = const (Just (intSize (Proxy @n))) isSigned = const True- testBit = undefined- bit = undefined+ testBit (IntN x) = testBit x+ bit = bitDefault popCount = undefined -instance FiniteBits Int30 where- finiteBitSize = const 30+instance (KnownNat n) => FiniteBits (IntN n) where+ finiteBitSize = const (intSize (Proxy @n))++data SomeIntN = forall n. (KnownNat n) ⇒ SomeIntN (IntN n)++instance Eq SomeIntN where+ SomeIntN (IntN @n1 i1) == SomeIntN (IntN @n2 i2) =+ case sameNat (Proxy @n1) (Proxy @n2) of+ Just _ → i1 == i2+ Nothing → False++instance Ord SomeIntN where+ SomeIntN (IntN @n1 i1) `compare` SomeIntN (IntN @n2 i2) =+ case cmpNat (Proxy @n1) (Proxy @n2) of+ LTI → LT+ EQI → compare i1 i2+ GTI → GT++instance Show SomeIntN where+ show (SomeIntN i) = show i++instance Arbitrary SomeIntN where+ arbitrary = do+ s <- chooseInt (8, 64)+ case someNatVal (toInteger s) of+ Just (SomeNat (Proxy ∷ Proxy n)) →+ SomeIntN <$> arbitraryBoundedIntegral @(IntN n)+ Nothing → error "impossible"+ shrink (SomeIntN i) = SomeIntN <$> shrinkIntegral i++--------------------------------------------------------------------------------+-- WordN+--------------------------------------------------------------------------------++newtype WordN (n ∷ Natural) = WordN' { unWordN :: Word64 }+ deriving stock (Eq, Ord)+ deriving newtype (Enum, Real, Integral)++instance (KnownNat n) ⇒ Show (WordN n) where+ showsPrec p (WordN x) = showParen (p > 10)+ (\s → mconcat ["WordN @", show (natVal (Proxy @n)), " ", show x, s])++pattern WordN ∷ forall n. (KnownNat n) => Word64 → WordN n+pattern WordN x ← WordN' x where+ WordN x = WordN' (x .&. ((1 `shiftL` intSize (Proxy @n)) - 1))++{-# COMPLETE WordN #-}++instance (KnownNat n) => Arbitrary (WordN n) where+ arbitrary =+ WordN <$> chooseBoundedIntegral (unWordN @n minBound, unWordN @n maxBound)+ shrink = shrinkIntegral++instance (KnownNat n) => Bounded (WordN n) where+ minBound = WordN' 0+ maxBound = WordN ((1 `shiftL` intSize (Proxy @n)) - 1)++instance (KnownNat n) => Num (WordN n) where+ WordN x + WordN y = WordN (x + y)+ WordN x * WordN y = WordN (x * y)+ abs = id+ signum = undefined+ negate (WordN x) = WordN (negate x)+ fromInteger x = WordN (fromInteger x)++instance (KnownNat n) => Bits (WordN n) where+ WordN a .&. WordN b = WordN (a .&. b)+ WordN a .|. WordN b = WordN (a .|. b)+ xor = undefined+ complement (WordN x) = WordN (complement x)+ shift (WordN x) i = WordN (shift x i)+ rotate = undefined+ bitSize = const (intSize (Proxy @n))+ bitSizeMaybe = const (Just (intSize (Proxy @n)))+ isSigned = const False+ testBit (WordN x) = testBit x+ bit = bitDefault+ popCount = undefined++instance (KnownNat n) => FiniteBits (WordN n) where+ finiteBitSize = const (intSize (Proxy @n))++data SomeWordN = forall n. (KnownNat n) ⇒ SomeWordN (WordN n)++instance Eq SomeWordN where+ SomeWordN (WordN @n1 i1) == SomeWordN (WordN @n2 i2) =+ case sameNat (Proxy @n1) (Proxy @n2) of+ Just _ → i1 == i2+ Nothing → False++instance Ord SomeWordN where+ SomeWordN (WordN @n1 i1) `compare` SomeWordN (WordN @n2 i2) =+ case cmpNat (Proxy @n1) (Proxy @n2) of+ LTI → LT+ EQI → compare i1 i2+ GTI → GT++instance Show SomeWordN where+ show (SomeWordN i) = show i++instance Arbitrary SomeWordN where+ arbitrary = do+ s <- chooseInt (8, 64)+ case someNatVal (toInteger s) of+ Just (SomeNat (Proxy ∷ Proxy n)) →+ SomeWordN <$> arbitraryBoundedIntegral @(WordN n)+ Nothing → error "impossible"+ shrink (SomeWordN i) = SomeWordN <$> shrinkIntegral i
text-builder-linear.cabal view
@@ -1,12 +1,12 @@ cabal-version: 2.4 name: text-builder-linear-version: 0.1.1.1+version: 0.1.2 license: BSD-3-Clause license-file: LICENSE copyright: 2022 Andrew Lelechenko maintainer: Andrew Lelechenko <andrew.lelechenko@gmail.com> author: Andrew Lelechenko-tested-with: ghc ==9.2.8 ghc ==9.4.7 ghc ==9.6.2 ghc ==9.8.1+tested-with: ghc ==9.2.8 ghc ==9.4.7 ghc ==9.6.3 ghc ==9.8.1 homepage: https://github.com/Bodigrim/linear-builder synopsis: Builder for Text and ByteString based on linear types description:@@ -30,6 +30,7 @@ hs-source-dirs: src other-modules:+ Data.Text.Builder.Linear.Array Data.Text.Builder.Linear.Char Data.Text.Builder.Linear.Dec Data.Text.Builder.Linear.Double@@ -63,7 +64,7 @@ base, text, text-builder-linear,- tasty >=1.4 && <1.5,+ tasty >=1.4 && <1.6, tasty-quickcheck >=0.10 && <0.11 benchmark linear-builder-bench@@ -85,5 +86,10 @@ bytestring, text, text-builder-linear,+ -- NOTE: The following packages are optional, but are not required that+ -- often. While they could be guarded by a flag, we prefer keeping+ -- the Hackage page simple. Just uncomment these lines when needed.+ -- bytestring-strict-builder >= 0.4.5 && < 0.5+ -- text-builder >= 0.6.7 && < 0.7, tasty, tasty-bench >=0.3.2 && <0.4