packages feed

text-builder-linear 0.1.2 → 0.1.4

raw patch · 20 files changed

Files

README.md view
@@ -44,7 +44,7 @@ Exponential growth provides for amortized linear time. Such structure can be implemented without linear types, but that would greatly affect user experience by polluting everything with `ST` monad.-Users are encouraged to use `Buffer` API, and built-in benchmarks refer to it.+Users are encouraged to use `Buffer` API, and **built-in benchmarks refer to it.**  The second interface is more traditional `newtype Builder = Builder (Buffer ⊸ Buffer)` with `Monoid` instance. This type provides easy migration from other builders,@@ -52,136 +52,52 @@ significantly faster than `Data.Text.Lazy.Builder`, as witnessed by benchmarks for `blaze-builder` below. -## Case study--Let's benchmark builders, which concatenate all `Char` from `minBound` to `maxBound`, producing a large `Text`:--```haskell-#!/usr/bin/env cabal-{- cabal:-build-depends: base, tasty-bench, text, text-builder, text-builder-linear-ghc-options: -O2--}--import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Builder as TLB-import qualified Text.Builder as TB-import qualified Data.Text.Builder.Linear as TBL-import System.Environment (getArgs)-import Test.Tasty.Bench--mkBench :: Monoid a => String -> (Char -> a) -> (a -> Int) -> Benchmark-mkBench s f g = bench s $ nf (g . foldMap f . enumFromTo minBound) maxBound-{-# INLINE mkBench #-}--main :: IO ()-main = defaultMain-  [ mkBench "text, lazy" TLB.singleton (fromIntegral . TL.length . TLB.toLazyText)-  , mkBench "text, strict" TLB.singleton (T.length . TL.toStrict . TLB.toLazyText)-  , mkBench "text-builder" TB.char (T.length . TB.run)-  , mkBench "text-builder-linear" TBL.fromChar (T.length . TBL.runBuilder)-  ]-```--Running this program with `cabal run Main.hs -- +RTS -T` yields following results:--```-text, lazy:-  4.25 ms ± 107 μs,  11 MB allocated, 912 B  copied-text, strict:-  7.18 ms ± 235 μs,  24 MB allocated,  10 MB copied-text-builder:-  80.1 ms ± 3.0 ms, 218 MB allocated, 107 MB copied-text-builder-linear:-  5.37 ms ± 146 μs,  44 MB allocated,  78 KB copied-```--The first result seems the best both in time and memory and corresponds to the-usual `Text` builder, where we do not materialize the entire result at all.-It builds chunks of lazy `Text` lazily and consumes them at once by-`TL.length`. Thus there are 11 MB of allocations in nursery, none of which-survive generation 0 garbage collector, so nothing is copied.--The second result is again the usual `Text` builder, but emulates a strict-consumer: we materialize a strict `Text` before computing length. Allocation-are doubled, and half of them (corresponding to the strict `Text`) survive to-the heap. Time is also almost twice longer, but still quite good.--The third result is for `text-builder` and demonstrates how bad things could-go with strict builders, aiming to precompute the precise length of the-buffer: allocating a thunk per char is tremendously slow and expensive.--The last result corresponds to the current package. We generate a strict-`Text` by growing and reallocating the buffer, thus allocations are quite-high. Nevertheless, it is already faster than the usual `Text` builder with-strict consumer and does not strain the garbage collector.--Things get very different if we remove `{-# INLINE mkBench #-}`:--```-text, lazy:-  36.9 ms ± 599 μs, 275 MB allocated,  30 KB copied-text, strict:-  44.7 ms ± 1.3 ms, 287 MB allocated,  25 MB copied-text-builder:-  77.6 ms ± 2.2 ms, 218 MB allocated, 107 MB copied-text-builder-linear:-  5.35 ms ± 212 μs,  44 MB allocated,  79 KB copied-```--Builders from `text` package degrade rapidly, 6-8x slower and 10-20x more-allocations. That's because their constant factors rely crucially on-everything getting inlined, which makes their performance fragile and-unreliable in large-scale applications. On the bright side of things, our-builder remains as fast as before and now is a clear champion.- ## Benchmarks for `Text` -Measured with GHC 9.6 on aarch64:+Measured with GHC 9.12 on aarch64:  |Group / size|`text`|`text-builder`|  |This package|  | |------------|-----:|-------------:|-:|-----------:|-:| | **Text** ||||||-|1|47.4 ns|24.2 ns|0.51x|35.2 ns|0.74x|-|10|509 ns|195 ns|0.38x|197 ns|0.39x|-|100|4.94 μs|1.74 μs|0.35x|1.66 μs|0.34x|-|1000|52.6 μs|17.0 μs|0.32x|15.0 μs|0.28x|-|10000|646 μs|206 μs|0.32x|155 μs|0.24x|-|100000|12.2 ms|3.34 ms|0.27x|2.60 ms|0.21x|-|1000000|159 ms|55.3 ms|0.35x|16.1 ms|0.10x|+|1|63.3 ns|30.8 ns|0.49x|60.5 ns|0.95x|+|10|764 ns|267 ns|0.35x|319 ns|0.42x|+|100|7.53 μs|2.48 μs|0.33x|2.61 μs|0.35x|+|1000|80.5 μs|26.7 μs|0.33x|23.1 μs|0.29x|+|10000|949 μs|319 μs|0.34x|242 μs|0.26x|+|100000|18.5 ms|8.22 ms|0.44x|2.36 ms|0.13x|+|1000000|216 ms|107 ms|0.49x|22.9 ms|0.11x| | **Char** ||||||-|1|46.9 ns|21.1 ns|0.45x|22.3 ns|0.48x|-|10|229 ns|152 ns|0.66x|79.9 ns|0.35x|-|100|2.00 μs|1.23 μs|0.61x|618 ns|0.31x|-|1000|21.9 μs|10.3 μs|0.47x|6.28 μs|0.29x|-|10000|285 μs|153 μs|0.54x|68.5 μs|0.24x|-|100000|7.70 ms|4.08 ms|0.53x|992 μs|0.13x|-|1000000|110 ms|106 ms|0.96x|9.19 ms|0.08x|+|1|49.0 ns|34.8 ns|0.71x|38.1 ns|0.78x|+|10|365 ns|293 ns|0.80x|117 ns|0.32x|+|100|3.20 μs|2.38 μs|0.74x|804 ns|0.25x|+|1000|35.4 μs|18.4 μs|0.52x|7.68 μs|0.22x|+|10000|460 μs|265 μs|0.58x|86.5 μs|0.19x|+|100000|12.7 ms|6.96 ms|0.55x|930 μs|0.07x|+|1000000|175 ms|178 ms|1.02x|10.5 ms|0.06x| | **Decimal** ||||||-|1|97.7 ns|872 ns|8.92x|80.2 ns|0.82x|-|10|864 ns|8.72 μs|10.09x|684 ns|0.79x|-|100|9.07 μs|93.5 μs|10.32x|7.25 μs|0.80x|-|1000|92.4 μs|1.06 ms|11.44x|67.5 μs|0.73x|-|10000|1.13 ms|13.4 ms|11.88x|667 μs|0.59x|-|100000|18.7 ms|141 ms|7.57x|7.57 ms|0.41x|-|1000000|229 ms|1.487 s|6.48x|67.8 ms|0.30x|+|1|148 ns|490 ns|3.30x|126 ns|0.85x|+|10|1.34 μs|4.80 μs|3.57x|1.07 μs|0.80x|+|100|14.3 μs|53.6 μs|3.76x|10.8 μs|0.76x|+|1000|148 μs|738 μs|5.00x|106 μs|0.72x|+|10000|1.66 ms|19.8 ms|11.96x|1.05 ms|0.63x|+|100000|28.3 ms|251 ms|8.88x|10.7 ms|0.38x|+|1000000|334 ms|2.803 s|8.40x|108 ms|0.32x| | **Hexadecimal** ||||||-|1|403 ns|749 ns|1.86x|43.9 ns|0.11x|-|10|3.94 μs|7.66 μs|1.94x|308 ns|0.08x|-|100|42.8 μs|89.0 μs|2.08x|2.88 μs|0.07x|-|1000|486 μs|986 μs|2.03x|27.7 μs|0.06x|-|10000|7.10 ms|12.6 ms|1.77x|283 μs|0.04x|-|100000|80.1 ms|133 ms|1.65x|3.53 ms|0.04x|-|1000000|867 ms|1.340 s|1.55x|28.9 ms|0.03x|+|1|711 ns|81.2 ns|0.11x|74.2 ns|0.10x|+|10|7.06 μs|795 ns|0.11x|510 ns|0.07x|+|100|76.3 μs|8.04 μs|0.11x|4.62 μs|0.06x|+|1000|862 μs|141 μs|0.16x|44.6 μs|0.05x|+|10000|12.4 ms|1.73 ms|0.14x|451 μs|0.04x|+|100000|138 ms|20.4 ms|0.15x|4.52 ms|0.03x|+|1000000|1.502 s|228 ms|0.15x|45.9 ms|0.03x| | **Double** ||||||-|1|7.56 μs|18.3 μs|2.42x|414 ns|0.05x|-|10|76.5 μs|188 μs|2.46x|4.23 μs|0.06x|-|100|754 μs|2.35 ms|3.11x|44.4 μs|0.06x|-|1000|7.94 ms|25.8 ms|3.25x|436 μs|0.05x|-|10000|79.1 ms|285 ms|3.60x|4.90 ms|0.06x|-|100000|796 ms|2.938 s|3.69x|45.1 ms|0.06x|-|1000000|8.003 s|32.411 s|4.05x|436 ms|0.05x|+|1|13.4 μs|35.3 μs|2.63x|638 ns|0.05x|+|10|137 μs|393 μs|2.88x|6.52 μs|0.05x|+|100|1.35 ms|5.62 ms|4.15x|67.9 μs|0.05x|+|1000|14.2 ms|71.9 ms|5.05x|671 μs|0.05x|+|10000|143 ms|750 ms|5.25x|7.18 ms|0.05x|+|100000|1.435 s|7.941 s|5.53x|70.4 ms|0.05x|+|1000000|14.366 s|101.342 s|7.05x|689 ms|0.05x|  If you are not convinced by synthetic data, here are benchmarks for@@ -207,53 +123,55 @@ ## Benchmarks for `ByteString`  Somewhat surprisingly, `text-builder-linear` now offers rendering to strict `ByteString`-as well. It is consistently faster than `bytestring` when a string gets over 32k+as well. It gets consistently faster than `bytestring`+in all benchmarks except `Double` ones+once a string gets over 32k (which is `defaultChunkSize` for `bytestring` builder). For mid-sized strings `bytestring` is slightly faster in certain disciplines, mostly by virtue of using `cbits` via FFI, while this package remains 100% native Haskell. -Benchmarks below were measured with GHC 9.6 on aarch64 and include comparison+Benchmarks below were measured with GHC 9.12 on aarch64 and include comparison to [`bytestring-strict-builder`](https://hackage.haskell.org/package/bytestring-strict-builder):  |Group / size|`bytestring`|`…-strict-builder`|  |This package|  | |------------|-----------:|-----------------:|-:|-----------:|-:| | **Text** ||||||-|1|106 ns|33.5 ns|0.32x|35.2 ns|0.33x|-|10|322 ns|217 ns|0.68x|197 ns|0.61x|-|100|2.49 μs|1.89 μs|0.76x|1.66 μs|0.67x|-|1000|21.8 μs|18.5 μs|0.85x|15.0 μs|0.69x|-|10000|231 μs|212 μs|0.92x|155 μs|0.67x|-|100000|3.97 ms|3.54 ms|0.89x|2.60 ms|0.66x|-|1000000|81.2 ms|51.5 ms|0.63x|16.1 ms|0.20x|+|1|156 ns|55.9 ns|0.36x|60.5 ns|0.39x|+|10|552 ns|374 ns|0.68x|319 ns|0.58x|+|100|4.71 μs|3.25 μs|0.69x|2.61 μs|0.55x|+|1000|41.7 μs|31.9 μs|0.76x|23.1 μs|0.56x|+|10000|438 μs|366 μs|0.84x|242 μs|0.55x|+|100000|7.58 ms|6.52 ms|0.86x|2.36 ms|0.31x|+|1000000|112 ms|88.1 ms|0.78x|22.9 ms|0.20x| | **Char** ||||||-|1|99.0 ns|19.4 ns|0.20x|22.3 ns|0.23x|-|10|270 ns|82.9 ns|0.31x|79.9 ns|0.30x|-|100|1.77 μs|723 ns|0.41x|618 ns|0.35x|-|1000|20.4 μs|8.37 μs|0.41x|6.28 μs|0.31x|-|10000|322 μs|129 μs|0.40x|68.5 μs|0.21x|-|100000|10.4 ms|2.50 ms|0.24x|992 μs|0.10x|-|1000000|143 ms|67.4 ms|0.47x|9.19 ms|0.06x|+|1|138 ns|30.9 ns|0.22x|38.1 ns|0.28x|+|10|408 ns|136 ns|0.33x|117 ns|0.29x|+|100|2.96 μs|1.25 μs|0.42x|804 ns|0.27x|+|1000|30.4 μs|14.2 μs|0.47x|7.68 μs|0.25x|+|10000|394 μs|218 μs|0.55x|86.5 μs|0.22x|+|100000|11.8 ms|4.00 ms|0.34x|930 μs|0.08x|+|1000000|161 ms|112 ms|0.69x|10.5 ms|0.07x| | **Decimal** ||||||-|1|152 ns|174 ns|1.14x|80.2 ns|0.53x|-|10|685 ns|1.55 μs|2.26x|684 ns|1.00x|-|100|5.88 μs|17.2 μs|2.93x|7.25 μs|1.23x|-|1000|60.3 μs|196 μs|3.25x|67.5 μs|1.12x|-|10000|648 μs|4.25 ms|6.57x|667 μs|1.03x|-|100000|11.2 ms|62.8 ms|5.62x|7.57 ms|0.68x|-|1000000|150 ms|655 ms|4.37x|67.8 ms|0.45x|+|1|209 ns|295 ns|1.41x|126 ns|0.60x|+|10|1.10 μs|2.67 μs|2.43x|1.07 μs|0.98x|+|100|9.76 μs|29.8 μs|3.05x|10.8 μs|1.11x|+|1000|100 μs|340 μs|3.40x|106 μs|1.06x|+|10000|1.02 ms|7.32 ms|7.15x|1.05 ms|1.02x|+|100000|14.6 ms|103 ms|7.04x|10.7 ms|0.73x|+|1000000|179 ms|1.233 s|6.87x|108 ms|0.60x| | **Hexadecimal** ||||||-|1|94.7 ns|||43.9 ns|0.46x|-|10|255 ns|||308 ns|1.21x|-|100|1.72 μs|||2.88 μs|1.67x|-|1000|18.9 μs|||27.7 μs|1.46x|-|10000|250 μs|||283 μs|1.13x|-|100000|6.94 ms|||3.53 ms|0.51x|-|1000000|93.2 ms|||28.9 ms|0.31x|+|1|131 ns|||74.2 ns|0.57x|+|10|360 ns|||510 ns|1.42x|+|100|2.76 μs|||4.62 μs|1.68x|+|1000|28.6 μs|||44.6 μs|1.56x|+|10000|330 μs|||451 μs|1.37x|+|100000|7.30 ms|||4.52 ms|0.62x|+|1000000|103 ms|||45.9 ms|0.45x| | **Double** ||||||-|1|457 ns|||414 ns|0.91x|-|10|3.94 μs|||4.23 μs|1.07x|-|100|40.3 μs|||44.4 μs|1.10x|-|1000|398 μs|||436 μs|1.10x|-|10000|5.65 ms|||4.90 ms|0.87x|-|100000|63.3 ms|||45.1 ms|0.71x|-|1000000|673 ms|||436 ms|0.65x|+|1|456 ns|||638 ns|1.40x|+|10|3.58 μs|||6.52 μs|1.82x|+|100|36.2 μs|||67.9 μs|1.87x|+|1000|367 μs|||671 μs|1.83x|+|10000|5.17 ms|||7.18 ms|1.39x|+|100000|59.0 ms|||70.4 ms|1.19x|+|1000000|605 ms|||689 ms|1.14x|
bench/BenchChar.hs view
@@ -10,16 +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-import qualified Text.Builder+import qualified TextBuilder #endif  #ifdef MIN_VERSION_bytestring_strict_builder@@ -44,10 +40,10 @@  #ifdef MIN_VERSION_text_builder benchStrictBuilder ∷ Int → T.Text-benchStrictBuilder = Text.Builder.run . go mempty+benchStrictBuilder = TextBuilder.toText . go mempty   where     go !acc 0 = acc-    go !acc n = let ch = chr n in go (Text.Builder.char ch <> (acc <> Text.Builder.char ch)) (n - 1)+    go !acc n = let ch = chr n in go (TextBuilder.char ch <> (acc <> TextBuilder.char ch)) (n - 1) #endif  #ifdef MIN_VERSION_bytestring_strict_builder@@ -65,15 +61,15 @@     go !acc 0 = acc     go !acc n = let ch = chr n in go (ch .<| (acc |>. ch)) (n - 1) -benchSingleChar ∷ Benchmark-benchSingleChar = bgroup "Single" $ map mkGroupChar [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6]+benchSingleChar ∷ [Benchmark]+benchSingleChar = map mkGroupChar [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6]  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-  , bench "Text.Builder" $ nf benchStrictBuilder n+  , bench "TextBuilder" $ nf benchStrictBuilder n #endif #ifdef MIN_VERSION_bytestring_strict_builder   , bench "ByteString.StrictBuilder" $ nf benchStrictBuilderBS n@@ -82,160 +78,8 @@   ]  ----------------------------------------------------------------------------------- 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 ]-+benchChar = bgroup "Char" benchSingleChar
bench/BenchDecimal.hs view
@@ -15,7 +15,7 @@ import Test.Tasty.Bench  #ifdef MIN_VERSION_text_builder-import qualified Text.Builder+import qualified TextBuilder #endif  #ifdef MIN_VERSION_bytestring_strict_builder@@ -39,10 +39,10 @@  #ifdef MIN_VERSION_text_builder benchStrictBuilder ∷ Int → T.Text-benchStrictBuilder = Text.Builder.run . go mempty+benchStrictBuilder = TextBuilder.toText . go mempty   where     go !acc 0 = acc-    go !acc n = let i = n * int in go (Text.Builder.decimal i <> (acc <> Text.Builder.decimal i)) (n - 1)+    go !acc n = let i = n * int in go (TextBuilder.decimal i <> (acc <> TextBuilder.decimal i)) (n - 1) #endif  #ifdef MIN_VERSION_bytestring_strict_builder@@ -68,7 +68,7 @@   [ 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+  , bench "TextBuilder" $ nf benchStrictBuilder n #endif #ifdef MIN_VERSION_bytestring_strict_builder   , bench "ByteString.StrictBuilder" $ nf benchStrictBuilderBS n
bench/BenchDouble.hs view
@@ -15,7 +15,8 @@ import Test.Tasty.Bench  #ifdef MIN_VERSION_text_builder-import qualified Text.Builder+import qualified TextBuilder+import qualified TextBuilderDev as TextBuilder #endif  dbl :: Double@@ -35,10 +36,10 @@  #ifdef MIN_VERSION_text_builder benchStrictBuilder ∷ Int → T.Text-benchStrictBuilder = Text.Builder.run . go mempty+benchStrictBuilder = TextBuilder.toText . go mempty   where     go !acc 0 = acc-    go !acc n = let d = fromIntegral n * dbl in go (Text.Builder.fixedDouble 17 d <> (acc <> Text.Builder.fixedDouble 17 d)) (n - 1)+    go !acc n = let d = fromIntegral n * dbl in go (TextBuilder.doubleFixedPoint 17 d <> (acc <> TextBuilder.doubleFixedPoint 17 d)) (n - 1) #endif  benchLinearBuilder ∷ Int → T.Text@@ -56,7 +57,7 @@   [ 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+  , bench "TextBuilder" $ nf benchStrictBuilder n #endif   , bench "Data.Text.Builder.Linear" $ nf benchLinearBuilder n   ]
bench/BenchHexadecimal.hs view
@@ -15,56 +15,48 @@ import Test.Tasty.Bench  #ifdef MIN_VERSION_text_builder-import qualified Text.Builder+import qualified TextBuilder #endif  word :: Word word = 123456789123456789 -benchLazyBuilder ∷ Word → T.Text+benchLazyBuilder ∷ Int → T.Text benchLazyBuilder = toStrict . toLazyText . go mempty   where     go !acc 0 = acc-    go !acc n = let i = n * word in go (hexadecimal i <> (acc <> hexadecimal i)) (n - 1)+    go !acc n = let i = fromIntegral n * word in go (hexadecimal i <> (acc <> hexadecimal i)) (n - 1) -benchLazyBuilderBS ∷ Word → B.ByteString+benchLazyBuilderBS ∷ Int → B.ByteString benchLazyBuilderBS = B.toStrict . B.toLazyByteString . go mempty   where     go !acc 0 = acc-    go !acc n = go (B.wordHex n <> (acc <> B.wordHex n)) (n - 1)+    go !acc n = go (B.wordHex (fromIntegral n) <> (acc <> B.wordHex (fromIntegral n))) (n - 1)  #ifdef MIN_VERSION_text_builder-benchStrictBuilder ∷ Word → T.Text-benchStrictBuilder = Text.Builder.run . go mempty+benchStrictBuilder ∷ Int → T.Text+benchStrictBuilder = TextBuilder.toText . go mempty   where     go !acc 0 = acc-    go !acc n = let i = n * word in go (Text.Builder.hexadecimal i <> (acc <> Text.Builder.hexadecimal i)) (n - 1)+    go !acc n = let i = fromIntegral n * word in go (TextBuilder.hexadecimal i <> (acc <> TextBuilder.hexadecimal i)) (n - 1) #endif -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))+benchLinearBuilder ∷ Int → T.Text+benchLinearBuilder m = runBuffer (\b → go b m)   where     go ∷ Buffer ⊸ Int → Buffer     go !acc 0 = acc-    go !acc n = let i = n * fromIntegral word in go (i &<| (acc |>& i)) (n - 1)+    go !acc n = let i = fromIntegral n * word in go (i &<| (acc |>& i)) (n - 1)  benchHexadecimal ∷ Benchmark benchHexadecimal = bgroup "Hexadecimal" $ map mkGroup [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6] -mkGroup :: Word → Benchmark+mkGroup :: Int → 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+  , bench "TextBuilder" $ nf benchStrictBuilder n #endif-  , bench "Data.Text.Builder.Linear (Word)" $ nf benchLinearBuilderWord n-  , bench "Data.Text.Builder.Linear (Int)" $ nf benchLinearBuilderInt n+  , bench "Data.Text.Builder.Linear" $ nf benchLinearBuilder n   ]
bench/BenchText.hs view
@@ -15,7 +15,7 @@ import Test.Tasty.Bench  #ifdef MIN_VERSION_text_builder-import qualified Text.Builder+import qualified TextBuilder #endif  #ifdef MIN_VERSION_bytestring_strict_builder@@ -41,9 +41,9 @@  #ifdef MIN_VERSION_text_builder benchStrictBuilder ∷ Int → T.Text-benchStrictBuilder = Text.Builder.run . go mempty+benchStrictBuilder = TextBuilder.toText . go mempty   where-    txtB = Text.Builder.text txt+    txtB = TextBuilder.text txt     go !acc 0 = acc     go !acc n = go (txtB <> (acc <> txtB)) (n - 1) #endif@@ -72,7 +72,7 @@   [ 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+  , bench "TextBuilder" $ nf benchStrictBuilder n #endif #ifdef MIN_VERSION_bytestring_strict_builder   , bench "ByteString.StrictBuilder" $ nf benchStrictBuilderBS n
changelog.md view
@@ -1,3 +1,12 @@+## 0.1.4++* Add `instance Eq Builder` and `instance Ord Builder`.+* Fix a bug in `dropBuffer` and `takeBuffer`.++## 0.1.3++* Add decimal builders for unbounded inputs: `fromUnboundedDec`, `(|>$$)` and `($$<|)`.+ ## 0.1.2  * Fix unsound behaviour caused by inlining of `runBuffer` / `runBufferBS`
src/Data/Text/Builder/Linear.hs view
@@ -1,9 +1,11 @@+{-# LANGUAGE CPP #-}+ -- | -- Copyright:   (c) 2022 Andrew Lelechenko -- Licence:     BSD3 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com> ----- Builder for strict 'Text' and 'ByteString', based on linear types. It consistently+-- Builder for strict t'Text' and 'ByteString', based on linear types. It consistently -- outperforms "Data.Text.Lazy.Builder" -- from @text@ as well as a strict builder from @text-builder@, -- and scales better.@@ -15,6 +17,7 @@   fromChar,   fromAddr,   fromDec,+  fromUnboundedDec,   fromHex,   fromDouble, ) where@@ -23,10 +26,15 @@ import Data.ByteString.Internal (ByteString (..)) import Data.Text.Internal (Text (..)) import GHC.Exts (Addr#, IsString (..))+#if MIN_VERSION_base(4,17,0)+import GHC.Exts (Multiplicity)+#else+import GHC.Base (Multiplicity)+#endif  import Data.Text.Builder.Linear.Buffer --- | Thin wrapper over 'Buffer' with a handy 'Semigroup' instance.+-- | Thin wrapper over t'Buffer' with a handy 'Semigroup' instance. -- -- >>> :set -XOverloadedStrings -XMagicHash -- >>> fromText "foo" <> fromChar '_' <> fromAddr "bar"#@@ -35,12 +43,24 @@ -- Remember: this is a strict builder, so on contrary to "Data.Text.Lazy.Builder" -- for optimal performance you should use strict left folds instead of lazy right ones. ----- Note that (similar to other builders) concatenation of 'Builder's allocates+-- Note that (similar to other builders) concatenation of t'Builder's allocates -- thunks. This is to a certain extent mitigated by aggressive inlining,--- but it is faster to use 'Buffer' directly.+-- but it is faster to use t'Buffer' directly. newtype Builder = Builder {unBuilder ∷ Buffer ⊸ Buffer} --- | Run 'Builder' computation on an empty 'Buffer', returning strict 'Text'.+-- | @since 0.1.4+instance Eq Builder where+  b1 == b2 = runBuilder b1 == runBuilder b2++-- | @since 0.1.4+instance Ord Builder where+  compare b1 b2 = compare (runBuilder b1) (runBuilder b2)+  b1 <= b2 = runBuilder b1 <= runBuilder b2+  b1 < b2 = runBuilder b1 < runBuilder b2+  b1 >= b2 = runBuilder b1 >= runBuilder b2+  b1 > b2 = runBuilder b1 > runBuilder b2++-- | Run t'Builder' computation on an empty t'Buffer', returning strict t'Text'. -- -- >>> :set -XOverloadedStrings -XMagicHash -- >>> runBuilder (fromText "foo" <> fromChar '_' <> fromAddr "bar"#)@@ -48,12 +68,12 @@ -- -- This function has a polymorphic arrow and thus can be used both in -- usual and linear contexts.-runBuilder ∷ ∀ m. Builder %m → Text+runBuilder ∷ ∀ (m ∷ Multiplicity). Builder %m → Text runBuilder (Builder f) = runBuffer f {-# INLINE runBuilder #-}  -- | Same as 'runBuilder', but returning a UTF-8 encoded strict 'ByteString'.-runBuilderBS ∷ ∀ m. Builder %m → ByteString+runBuilderBS ∷ ∀ (m ∷ Multiplicity). Builder %m → ByteString runBuilderBS (Builder f) = runBufferBS f {-# INLINE runBuilderBS #-} @@ -68,20 +88,23 @@   mempty = Builder (\b → b)   {-# INLINE mempty #-} +-- | Use 'fromString' to create t'Builder' from 'String'. instance IsString Builder where   fromString = fromText . fromString   {-# INLINE fromString #-} --- | Create 'Builder', containing a given 'Text'.+-- | Create t'Builder', containing a given t'Text'. -- -- >>> :set -XOverloadedStrings -- >>> fromText "foo" <> fromText "bar" -- "foobar"+--+-- For literal strings it is faster to use 'fromAddr' instead of 'fromText'. fromText ∷ Text → Builder fromText x = Builder $ \b → b |> x {-# INLINE fromText #-} --- | Create 'Builder', containing a given 'Char'.+-- | Create t'Builder', containing a given 'Char'. -- -- >>> fromChar 'x' <> fromChar 'y' -- "xy"@@ -92,19 +115,19 @@ fromChar x = Builder $ \b → b |>. x {-# INLINE fromChar #-} --- | Create 'Builder', containing a null-terminated UTF-8 string, specified by 'Addr#'.+-- | Create t'Builder', containing a null-terminated UTF-8 string, specified by 'Addr#'. -- -- >>> :set -XMagicHash -- >>> fromAddr "foo"# <> fromAddr "bar"# -- "foobar" ----- The literal string must not contain zero bytes @\\0@ and must be a valid UTF-8,+-- The literal string must not contain zero bytes @\\NUL@ and must be a valid UTF-8, -- these conditions are not checked. fromAddr ∷ Addr# → Builder fromAddr x = Builder $ \b → b |># x {-# INLINE fromAddr #-} --- | Create 'Builder', containing decimal representation of a given integer.+-- | Create t'Builder', containing decimal representation of a given /bounded/ integer. -- -- >>> fromChar 'x' <> fromDec (123 :: Int) -- "x123"@@ -112,8 +135,18 @@ fromDec x = Builder $ \b → b |>$ x {-# INLINE fromDec #-} --- | Create 'Builder', containing hexadecimal representation of a given integer.+-- | Create t'Builder', containing decimal representation of a given /unbounded/ integer. --+-- >>> fromChar 'x' <> fromUnboundedDec (1e24 :: Integer)+-- "x1000000000000000000000000"+--+-- @since 0.1.3+fromUnboundedDec ∷ Integral a ⇒ a → Builder+fromUnboundedDec x = Builder $ \b → b |>$$ x+{-# INLINE fromUnboundedDec #-}++-- | Create t'Builder', containing hexadecimal representation of a given integer.+-- -- >>> :set -XMagicHash -- >>> fromAddr "0x"# <> fromHex (0x123def :: Int) -- "0x123def"@@ -121,7 +154,7 @@ fromHex x = Builder $ \b → b |>& x {-# INLINE fromHex #-} --- | Create 'Builder', containing decimal representation of a given 'Double'.+-- | Create t'Builder', containing decimal representation of a given 'Double'. -- -- >>> :set -XMagicHash -- >>> fromAddr "pi="# <> fromDouble pi
src/Data/Text/Builder/Linear/Array.hs view
@@ -16,17 +16,16 @@ ) 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#)+#if MIN_VERSION_base(4,20,0)+import GHC.Exts (Int (..), isByteArrayPinned#, isTrue#, setByteArray#, sizeofByteArray#, unsafeThawByteArray#) #else-import GHC.Exts (unsafeCoerce#)+import GHC.Exts (Int (..), isByteArrayPinned#, isTrue#, setByteArray#, sizeofByteArray#, unsafeCoerce#) #endif  unsafeThaw ∷ A.Array → ST s (A.MArray s)-#if __GLASGOW_HASKELL__ >= 909+#if MIN_VERSION_base(4,20,0) unsafeThaw (A.ByteArray a) = ST $ \s# → case unsafeThawByteArray# a s# of   (# s'#, ma #) -> (# s'#, A.MutableByteArray ma #) #else
src/Data/Text/Builder/Linear/Buffer.hs view
@@ -4,7 +4,7 @@ -- Licence:     BSD3 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com> ----- 'Buffer' for strict 'Text', based on linear types.+-- t'Buffer' for strict t'Text', based on linear types. module Data.Text.Builder.Linear.Buffer (   -- * Type   Buffer,@@ -48,9 +48,15 @@   -- * Number formatting    -- ** Decimal++  -- *** Bounded numbers   (|>$),   ($<|), +  -- *** Unbounded numbers+  (|>$$),+  ($$<|),+   -- ** Hexadecimal    -- *** Lower-case@@ -72,11 +78,12 @@  import Data.Text.Builder.Linear.Char import Data.Text.Builder.Linear.Core-import Data.Text.Builder.Linear.Dec+import Data.Text.Builder.Linear.Dec.Bounded+import Data.Text.Builder.Linear.Dec.Unbounded import Data.Text.Builder.Linear.Double import Data.Text.Builder.Linear.Hex --- | Append 'Text' suffix to a 'Buffer' by mutating it.+-- | Append t'Text' suffix to a t'Buffer' by mutating it. -- If a suffix is statically known, consider using '(|>#)' for optimal performance. -- -- >>> :set -XOverloadedStrings -XLinearTypes@@ -91,7 +98,7 @@     (\dst dstOff → A.copyI srcLen dst dstOff src srcOff)     buffer --- | Prepend 'Text' prefix to a 'Buffer' by mutating it.+-- | Prepend t'Text' prefix to a t'Buffer' by mutating it. -- If a prefix is statically known, consider using '(#<|)' for optimal performance. -- -- >>> :set -XOverloadedStrings -XLinearTypes@@ -107,13 +114,13 @@     buffer  -- | Append a null-terminated UTF-8 string--- to a 'Buffer' by mutating it. E. g.,+-- to a t'Buffer' by mutating it. E. g., -- -- >>> :set -XOverloadedStrings -XLinearTypes -XMagicHash -- >>> runBuffer (\b -> b |># "foo"# |># "bar"#) -- "foobar" ----- The literal string must not contain zero bytes @\\0@ and must be a valid UTF-8,+-- The literal string must not contain zero bytes @\\NUL@ and must be a valid UTF-8, -- these conditions are not checked. (|>#) ∷ Buffer ⊸ Addr# → Buffer @@ -127,19 +134,19 @@     srcLen = I# (cstringLength# addr#)  -- | Prepend a null-terminated UTF-8 string--- to a 'Buffer' by mutating it. E. g.,+-- to a t'Buffer' by mutating it. E. g., -- -- >>> :set -XOverloadedStrings -XLinearTypes -XMagicHash -- >>> runBuffer (\b -> "foo"# #<| "bar"# #<| b) -- "foobar" ----- The literal string must not contain zero bytes @\\0@ and must be a valid UTF-8,+-- The literal string must not contain zero bytes @\\NUL@ and must be a valid UTF-8, -- these conditions are not checked. -- -- /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)”+-- “<https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/primitives.html#unboxed-tuples Unboxed types and primitive operations>” -- for further information. ( #<| ) ∷ Addr# → Buffer ⊸ Buffer @@ -201,7 +208,7 @@  -- $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).+-- Note that neither /upper/ case nor padded 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/blob/master/examples/src/Examples/Unicode.hs).
src/Data/Text/Builder/Linear/Char.hs view
@@ -31,7 +31,7 @@ -- Single char -------------------------------------------------------------------------------- --- | Append 'Char' to a 'Buffer' by mutating it.+-- | Append 'Char' to a t'Buffer' by mutating it. -- -- >>> :set -XLinearTypes -- >>> runBuffer (\b -> b |>. 'q' |>. 'w')@@ -45,7 +45,7 @@ infixl 6 |>. buffer |>. ch = appendBounded 4 (\dst dstOff → unsafeWrite dst dstOff ch) buffer --- | Prepend 'Char' to a 'Buffer' by mutating it.+-- | Prepend 'Char' to a t'Buffer' by mutating it. -- -- >>> :set -XLinearTypes -- >>> runBuffer (\b -> 'q' .<| 'w' .<| b)@@ -95,11 +95,15 @@ -- Multiple chars -------------------------------------------------------------------------------- --- | Prepend a given count of a 'Char' to a 'Buffer'.+-- | Prepend a given count of a 'Char' to a t'Buffer'. -- -- >>> :set -XLinearTypes -- >>> runBuffer (\b -> prependChars 3 'x' (b |>. 'A')) -- "xxxA"+--+-- __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'. prependChars ∷ Word → Char → Buffer ⊸ Buffer prependChars count ch buff   | count == 0 = buff@@ -115,11 +119,15 @@               )               buff --- | Apppend a given count of a 'Char' to a 'Buffer'.+-- | Apppend a given count of a 'Char' to a t'Buffer'. -- -- >>> :set -XLinearTypes -- >>> runBuffer (\b -> appendChars 3 'x' (b |>. 'A')) -- "Axxx"+--+-- __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'. appendChars ∷ Word → Char → Buffer ⊸ Buffer appendChars count ch buff   | count == 0 = buff@@ -162,8 +170,12 @@ --   (# b', empty #) -> b' >< justifyRight 12 ' ' (empty |> t) -- :} ----- >>> runBuffer (\b -> (b |> "Test:") `appendJustified` "foo" `appendJustified` "bar")--- "Test:         foo         bar"+-- >>> runBuffer (\b -> (b |> "Test:") `appendJustified` "AAA" `appendJustified` "BBBBBBB")+-- "Test:         AAA     BBBBBBB"+--+-- __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'. justifyRight ∷ Word → Char → Buffer ⊸ Buffer justifyRight n ch buff = case lengthOfBuffer buff of   (# buff', len #) →@@ -183,6 +195,10 @@ -- -- Note that 'newEmptyBuffer' is needed in some situations. See 'justifyRight' -- for an example.+--+-- __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'. justifyLeft ∷ Word → Char → Buffer ⊸ Buffer justifyLeft n ch buff = case lengthOfBuffer buff of   (# buff', len #) →@@ -201,6 +217,10 @@ -- -- Note that 'newEmptyBuffer' is needed in some situations. See 'justifyRight' -- for an example.+--+-- __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'. center ∷ Word → Char → Buffer ⊸ Buffer center n ch buff = case lengthOfBuffer buff of   (# buff', len #) →
src/Data/Text/Builder/Linear/Core.hs view
@@ -4,7 +4,7 @@ -- Licence:     BSD3 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com> ----- Low-level routines for 'Buffer' manipulations.+-- Low-level routines for t'Buffer' manipulations. module Data.Text.Builder.Linear.Core (   -- * Type   Buffer,@@ -29,315 +29,4 @@   (><), ) where -import Data.ByteString.Internal (ByteString (..))-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#, 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.--- That's why API below is carefully designed to prevent such possibility:--- clients always work with linear functions 'Buffer' ⊸ 'Buffer' instead--- and run them on an empty 'Buffer' to extract results.------ In terms of [@linear-base@](https://hackage.haskell.org/package/linear-base)--- 'Buffer' is [@Consumable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Consumable)--- (see 'consumeBuffer')--- and [@Dupable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Dupable)--- (see 'dupBuffer'),--- but not [@Movable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Movable).------ >>> :set -XOverloadedStrings -XLinearTypes--- >>> import Data.Text.Builder.Linear.Buffer--- >>> runBuffer (\b -> '!' .<| "foo" <| (b |> "bar" |>. '.'))--- "!foobar."------ Remember: this is a strict builder, so on contrary to "Data.Text.Lazy.Builder"--- for optimal performance you should use strict left folds instead of lazy right ones.------ 'Buffer' is an unlifted datatype,--- so you can put it into an unboxed tuple @(# ..., ... #)@,--- but not into @(..., ...)@.-data Buffer ∷ TYPE ('BoxedRep 'Unlifted) where-  Buffer ∷ {-# UNPACK #-} !Text → Buffer---- | Unwrap 'Buffer', no-op.--- Most likely, this is not the function you're looking for--- and you need 'runBuffer' instead.-unBuffer ∷ Buffer ⊸ Text-unBuffer (Buffer x) = x---- | Run a linear function on an empty 'Buffer', producing a strict 'Text'.------ 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 -> ...@.--- 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).------ 'runBuffer' is similar in spirit to mutable arrays API in--- [@Data.Array.Mutable.Linear@](https://hackage.haskell.org/package/linear-base/docs/Data-Array-Mutable-Linear.html),--- which provides functions like--- [@fromList@](https://hackage.haskell.org/package/linear-base/docs/Data-Array-Mutable-Linear.html#v:fromList) ∷ [@a@] → (@Vector@ @a@ ⊸ [@Ur@](https://hackage.haskell.org/package/linear-base-0.3.0/docs/Prelude-Linear.html#t:Ur) b) ⊸ [@Ur@](https://hackage.haskell.org/package/linear-base-0.3.0/docs/Prelude-Linear.html#t:Ur) @b@.--- Here the initial buffer is always empty and @b@ is 'Text'. Since 'Text' is--- [@Movable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Movable),--- '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-  Buffer (Text (A.ByteArray arr) (I# from) len) → BS fp len-    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-  arrM ← unsafeThaw arr-  A.shrinkM arrM (from + len)-  arr' ← A.unsafeFreeze arrM-  pure $ Text arr' from len--memptyPinned ∷ Text-memptyPinned = runST $ do-  marr ← A.newPinned 0-  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)--- from [@linear-base@](https://hackage.haskell.org/package/linear-base).------ It is a bit tricky to use because of--- <https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/linear_types.html#limitations current limitations>--- of linear types with regards to @let@ and @where@. E. g., one cannot write------ > let (# b1, b2 #) = dupBuffer b in ("foo" <| b1) >< (b2 |> "bar")------ Instead write:------ >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples--- >>> import Data.Text.Builder.Linear.Buffer--- >>> runBuffer (\b -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar"))--- "foobar"------ Note the unboxed tuple: 'Buffer' is an unlifted datatype,--- so it cannot be put into @(..., ...)@.-dupBuffer ∷ Buffer ⊸ (# Buffer, Buffer #)-dupBuffer (Buffer x) = (# Buffer x, Buffer (T.copy x) #)---- | Consume buffer linearly,--- similar to--- [@Consumable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Consumable)--- from [@linear-base@](https://hackage.haskell.org/package/linear-base).-consumeBuffer ∷ Buffer ⊸ ()-consumeBuffer Buffer {} = ()---- | Erase buffer's content, replacing it with an empty 'Text'.-eraseBuffer ∷ Buffer ⊸ Buffer-eraseBuffer (Buffer (Text arr _ _)) =-  Buffer (if isPinned arr then memptyPinned else mempty)---- | Return buffer's size in __bytes__ (not in 'Char's).--- This could be useful to implement a lazy builder atop of a strict one.-byteSizeOfBuffer ∷ Buffer ⊸ (# Buffer, Word #)-byteSizeOfBuffer (Buffer t@(Text _ _ len)) = (# Buffer t, fromIntegral len #)---- | Return buffer's length in 'Char's (not in bytes).--- This could be useful to implement @dropEndBuffer@ and @takeEndBuffer@, e. g.,------ @--- import Data.Unrestricted.Linear------ dropEndBuffer :: Word -> Buffer %1 -> Buffer--- 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) #)---- | Slice 'Buffer' by dropping given number of 'Char's.-dropBuffer ∷ Word → Buffer ⊸ Buffer-dropBuffer nChar (Buffer t@(Text arr off len))-  | nByte <= 0 = Buffer (Text arr (off + len) 0)-  | otherwise = Buffer (Text arr (off + nByte) (len - nByte))-  where-    nByte = T.measureOff (fromIntegral nChar) t---- | Slice 'Buffer' by taking given number of 'Char's.-takeBuffer ∷ Word → Buffer ⊸ Buffer-takeBuffer nChar (Buffer t@(Text arr off _))-  | nByte <= 0 = Buffer t-  | otherwise = Buffer (Text arr off nByte)-  where-    nByte = T.measureOff (fromIntegral nChar) t---- | Low-level routine to append data of unknown size to a 'Buffer'.-appendBounded-  ∷ Int-  -- ^ Upper bound for the number of bytes, written by an action-  → (∀ s. A.MArray s → Int → ST s Int)-  -- ^ Action, which writes bytes __starting__ from the given offset-  -- and returns an actual number of bytes written.-  → Buffer-  ⊸ Buffer-appendBounded maxSrcLen appender (Buffer (Text dst dstOff dstLen)) = Buffer $ runST $ do-  let dstFullLen = sizeofByteArray dst-      newFullLen = dstOff + 2 * (dstLen + maxSrcLen)-  newM ←-    if dstOff + dstLen + maxSrcLen <= dstFullLen-      then unsafeThaw dst-      else do-        tmpM ← (if isPinned dst then A.newPinned else A.new) newFullLen-        A.copyI dstLen tmpM dstOff dst dstOff-        pure tmpM-  srcLen ← appender newM (dstOff + dstLen)-  new ← A.unsafeFreeze newM-  pure $ Text new dstOff (dstLen + srcLen)-{-# INLINE appendBounded #-}---- | Low-level routine to append data of known size to a 'Buffer'.-appendExact-  ∷ Int-  -- ^ Exact number of bytes, written by an action-  → (∀ s. A.MArray s → Int → ST s ())-  -- ^ Action, which writes bytes __starting__ from the given offset-  → Buffer-  ⊸ Buffer-appendExact srcLen appender =-  appendBounded-    srcLen-    (\dst dstOff → appender dst dstOff >> pure srcLen)-{-# INLINE appendExact #-}---- | Low-level routine to prepend data of unknown size to a 'Buffer'.-prependBounded-  ∷ Int-  -- ^ Upper bound for the number of bytes, written by an action-  → (∀ s. A.MArray s → Int → ST s Int)-  -- ^ Action, which writes bytes __finishing__ before the given offset-  -- and returns an actual number of bytes written.-  → (∀ s. A.MArray s → Int → ST s Int)-  -- ^ Action, which writes bytes __starting__ from the given offset-  -- and returns an actual number of bytes written.-  → Buffer-  ⊸ Buffer-prependBounded maxSrcLen prepender appender (Buffer (Text dst dstOff dstLen))-  | maxSrcLen <= dstOff = Buffer $ runST $ do-      newM ← unsafeThaw dst-      srcLen ← prepender newM dstOff-      new ← A.unsafeFreeze newM-      pure $ Text new (dstOff - srcLen) (srcLen + dstLen)-  | otherwise = Buffer $ runST $ do-      let dstFullLen = sizeofByteArray dst-          newOff = dstLen + maxSrcLen-          newFullLen = 2 * newOff + (dstFullLen - dstOff - dstLen)-      newM ← (if isPinned dst then A.newPinned else A.new) newFullLen-      srcLen ← appender newM newOff-      A.copyI dstLen newM (newOff + srcLen) dst dstOff-      new ← A.unsafeFreeze newM-      pure $ Text new newOff (dstLen + srcLen)-{-# INLINE prependBounded #-}---- | Low-level routine to append data of known size to a 'Buffer'.-prependExact-  ∷ Int-  -- ^ Exact number of bytes, written by an action-  → (∀ s. A.MArray s → Int → ST s ())-  -- ^ Action, which writes bytes __starting__ from the given offset-  → Buffer-  ⊸ Buffer-prependExact srcLen appender =-  prependBounded-    srcLen-    (\dst dstOff → appender dst (dstOff - srcLen) >> pure srcLen)-    (\dst dstOff → appender dst dstOff >> pure srcLen)-{-# INLINE prependExact #-}---- | 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 -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar"))--- "foobar"-(><) ∷ Buffer ⊸ Buffer ⊸ Buffer--infix 6 ><-Buffer (Text left leftOff leftLen) >< Buffer (Text right rightOff rightLen) = Buffer $ runST $ do-  let leftFullLen = sizeofByteArray left-      rightFullLen = sizeofByteArray right-      canCopyToLeft = leftOff + leftLen + rightLen <= leftFullLen-      canCopyToRight = leftLen <= rightOff-      shouldCopyToLeft = canCopyToLeft && (not canCopyToRight || leftLen >= rightLen)-  if shouldCopyToLeft-    then do-      newM ← unsafeThaw left-      A.copyI rightLen newM (leftOff + leftLen) right rightOff-      new ← A.unsafeFreeze newM-      pure $ Text new leftOff (leftLen + rightLen)-    else-      if canCopyToRight-        then do-          newM ← unsafeThaw right-          A.copyI leftLen newM (rightOff - leftLen) left leftOff-          new ← A.unsafeFreeze newM-          pure $ Text new (rightOff - leftLen) (leftLen + rightLen)-        else do-          let fullLen = leftOff + leftLen + rightLen + (rightFullLen - rightOff - rightLen)-          newM ← (if isPinned left || isPinned right then A.newPinned else A.new) fullLen-          A.copyI leftLen newM leftOff left leftOff-          A.copyI rightLen newM (leftOff + leftLen) right rightOff-          new ← A.unsafeFreeze newM-          pure $ Text new leftOff (leftLen + rightLen)+import Data.Text.Builder.Linear.Internal
− src/Data/Text/Builder/Linear/Dec.hs
@@ -1,172 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE TemplateHaskell #-}---- |--- Copyright:   (c) 2022 Andrew Lelechenko--- Licence:     BSD3--- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>-#ifdef aarch64_HOST_ARCH-{-# OPTIONS_GHC -Wno-unused-imports -Wno-unused-top-binds #-}-#endif--module Data.Text.Builder.Linear.Dec (-  (|>$),-  ($<|),-) where--#include "MachDeps.h"--import Data.Bits (Bits (..), FiniteBits (..))-import Data.Int (Int16, Int32, Int8)-import Data.Text.Array qualified as A-import Data.Word (Word16, Word32, Word8)-import GHC.Exts (Addr#, Int (..), Ptr (..), dataToTag#, (>=#))-import GHC.Ptr (plusPtr)-import GHC.ST (ST)-import Numeric.QuoteQuot (assumeNonNegArg, astQuot, quoteAST, quoteQuot)--import Data.Text.Builder.Linear.Core---- | Append decimal number.-(|>$) ∷ (Integral a, FiniteBits a) ⇒ Buffer ⊸ a → Buffer--infixl 6 |>$-buffer |>$ n =-  appendBounded-    (maxDecLen n)-    (\dst dstOff → unsafeAppendDec dst dstOff n)-    buffer-{-# INLINEABLE (|>$) #-}---- | Prepend decimal number.-($<|) ∷ (Integral a, FiniteBits a) ⇒ a → Buffer ⊸ Buffer--infixr 6 $<|-n $<| buffer =-  prependBounded-    (maxDecLen n)-    (\dst dstOff → unsafePrependDec dst dstOff n)-    (\dst dstOff → unsafeAppendDec dst dstOff n)-    buffer-{-# INLINEABLE ($<|) #-}---- | ceiling (fbs a * logBase 10 2) < ceiling (fbs a * 5 / 16) < 1 + floor (fbs a * 5 / 16)-maxDecLen ∷ FiniteBits a ⇒ a → Int-maxDecLen a-  | isSigned a = 2 + (finiteBitSize a * 5) `shiftR` 4-  | otherwise = 1 + (finiteBitSize a * 5) `shiftR` 4-{-# INLINEABLE maxDecLen #-}--exactDecLen ∷ (Integral a, FiniteBits a) ⇒ a → Int-exactDecLen n-  | n < 0 =-      go 2 (complement n + fromIntegral (I# (dataToTag# (n > bit (finiteBitSize n - 1)))))-  | otherwise =-      go 1 n-  where-    go ∷ (Integral a, FiniteBits a) ⇒ Int → a → Int-    go acc 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# >=# 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-unsafeAppendDec marr off n = unsafePrependDec marr (off + exactDecLen n) n-{-# INLINEABLE unsafeAppendDec #-}--unsafePrependDec ∷ ∀ s a. (Integral a, FiniteBits a) ⇒ A.MArray s → Int → a → ST s Int-unsafePrependDec marr !off n-  | n < 0-  , n == bit (finiteBitSize n - 1) = do-      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-  | otherwise = go (off - 1) (abs n) >>= sign-  where-    sign !o-      | n > 0 = pure (off - o)-      | otherwise = do-          A.unsafeWrite marr (o - 1) 0x2d -- '-'-          pure (off - o + 1)--    go ∷ Int → a → ST s Int-    go o k-      | k >= 10 = do-          let (q, r) = quotRem100 k-          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 (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 .&. 3 of-  0 → 8-  1 → 6-  2 → 2-  _ → 4-{-# INLINEABLE minBoundLastDigit #-}--quotRem100 ∷ (Integral a, FiniteBits a) ⇒ a → (a, a)---- https://gitlab.haskell.org/ghc/ghc/-/issues/22933-#ifdef aarch64_HOST_ARCH-quotRem100 a = a `quotRem` 100-#else-quotRem100 a = let q = quot100 a in (q, a - 100 * q)-#endif-{-# INLINEABLE quotRem100 #-}--quot100 ∷ (Integral a, FiniteBits a) ⇒ a → a-quot100 a = case (finiteBitSize a, isSigned a) of-  (64, True)-    | finiteBitSize (0 ∷ Int) == 64 →-        cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int))-  (64, False)-    | finiteBitSize (0 ∷ Word) == 64 →-        cast $$(quoteQuot (100 ∷ Word))-  (32, True) → cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int32))-  (32, False) → cast $$(quoteQuot (100 ∷ Word32))-  (16, True) → cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int16))-  (16, False) → cast $$(quoteQuot (100 ∷ Word16))-  (8, True) → cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int8))-  (8, False) → cast $$(quoteQuot (100 ∷ Word8))-  _ → a `quot` 100-  where-    cast ∷ (Integral a, Integral b) ⇒ (b → b) → a-    cast f = fromIntegral (f (fromIntegral a))-{-# INLINEABLE quot100 #-}--quotBillion ∷ (Integral a, FiniteBits a) ⇒ a → a-#ifdef aarch64_HOST_ARCH-quotBillion a = a `quot` 1e9-#else-quotBillion a = case (finiteBitSize a, isSigned a) of-  (64, True)-    | finiteBitSize (0 :: Int) == 64-    → cast $$(quoteAST $ assumeNonNegArg $ astQuot (1e9 :: Int))-  (64, False)-    | finiteBitSize (0 :: Word) == 64-    → cast $$(quoteQuot (1e9 :: Word))-  (32, True)  → cast $$(quoteAST $ assumeNonNegArg $ astQuot (1e9 :: Int32))-  (32, False) → cast $$(quoteQuot (1e9 :: Word32))-  _ → a `quot` 1e9-  where-    cast :: (Integral a, Integral b) => (b → b) → a-    cast f = fromIntegral (f (fromIntegral a))-#endif-{-# INLINEABLE quotBillion #-}
+ src/Data/Text/Builder/Linear/Dec/Bounded.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Copyright:   (c) 2022 Andrew Lelechenko+-- Licence:     BSD3+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>+#ifdef aarch64_HOST_ARCH+{-# OPTIONS_GHC -Wno-unused-imports -Wno-unused-top-binds #-}+#endif++module Data.Text.Builder.Linear.Dec.Bounded (+  (|>$),+  ($<|),+  unsafePrependDec,+  unsafeAppendDec,+  maxDecLen,+  quotRem100,+  digits,+) where++#include "MachDeps.h"++import Data.Bits (Bits (..), FiniteBits (..))+import Data.Int (Int16, Int32, Int8)+import Data.Text.Array qualified as A+import Data.Word (Word16, Word32, Word8)+import Foreign.C.String (CString)+import GHC.Exts (Int (..), Ptr (..), dataToTag#, (>=#))+import GHC.Ptr (plusPtr)+import GHC.ST (ST (..))+import Numeric.QuoteQuot (assumeNonNegArg, astQuot, quoteAST, quoteQuot)++import Data.Text.Builder.Linear.Core++-- | Append the decimal representation of a /bounded/ integral number.+(|>$) ∷ (Integral a, FiniteBits a) ⇒ Buffer ⊸ a → Buffer++infixl 6 |>$+buffer |>$ n =+  appendBounded+    (maxDecLen n)+    (\dst dstOff → unsafeAppendDec dst dstOff n)+    buffer+{-# INLINEABLE (|>$) #-}++-- | Prepend the decimal representation of a /bounded/ integral number.+($<|) ∷ (Integral a, FiniteBits a) ⇒ a → Buffer ⊸ Buffer++infixr 6 $<|+n $<| buffer =+  prependBounded+    (maxDecLen n)+    (\dst dstOff → unsafePrependDec dst dstOff n)+    (\dst dstOff → unsafeAppendDec dst dstOff n)+    buffer+{-# INLINEABLE ($<|) #-}++-- | ceiling (fbs a * logBase 10 2) < ceiling (fbs a * 5 / 16) < 1 + floor (fbs a * 5 / 16)+maxDecLen ∷ FiniteBits a ⇒ a → Int+maxDecLen a+  | isSigned a = 2 + (finiteBitSize a * 5) `shiftR` 4+  | otherwise = 1 + (finiteBitSize a * 5) `shiftR` 4+{-# INLINEABLE maxDecLen #-}++exactDecLen ∷ (Integral a, FiniteBits a) ⇒ a → Int+exactDecLen n+  | n < 0 =+      go 2 (complement n + fromIntegral (I# (dataToTag# (n > bit (finiteBitSize n - 1)))))+  | otherwise =+      go 1 n+  where+    go ∷ (Integral a, FiniteBits a) ⇒ Int → a → Int+    go acc k+      | finiteBitSize k >= if isSigned k then 31 else 30, k >= 1e9 = go (acc + 9) (quotBillion k)+      | otherwise = acc + exactIntDecLen (fromIntegral k)++    exactIntDecLen ∷ Int → Int+    exactIntDecLen l@(I# l#)+      | 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+unsafeAppendDec marr off n = unsafePrependDec marr (off + exactDecLen n) n+{-# INLINEABLE unsafeAppendDec #-}++unsafePrependDec ∷ ∀ s a. (Integral a, FiniteBits a) ⇒ A.MArray s → Int → a → ST s Int+unsafePrependDec marr !off n+  | n < 0+  , n == bit (finiteBitSize n - 1) = do+      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+  | otherwise = go (off - 1) (abs n) >>= sign+  where+    sign !o+      | n > 0 = pure (off - o)+      | otherwise = do+          A.unsafeWrite marr (o - 1) 0x2d -- '-'+          pure (off - o + 1)++    go ∷ Int → a → ST s Int+    go o k+      | k >= 10 = do+          let (q, r) = quotRem100 k+          A.copyFromPointer marr (o - 1) (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 (0x30 + k))+          pure o+{-# INLINEABLE unsafePrependDec #-}++digits ∷ CString+digits = Ptr "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899"#+{-# NOINLINE digits #-}++-- 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 .&. 3 of+  0 → 8+  1 → 6+  2 → 2+  _ → 4+{-# INLINEABLE minBoundLastDigit #-}++quotRem100 ∷ (Integral a, FiniteBits a) ⇒ a → (a, a)++-- https://gitlab.haskell.org/ghc/ghc/-/issues/22933+#ifdef aarch64_HOST_ARCH+quotRem100 a = a `quotRem` 100+#else+quotRem100 a = let q = quot100 a in (q, a - 100 * q)+#endif+{-# INLINEABLE quotRem100 #-}++quot100 ∷ (Integral a, FiniteBits a) ⇒ a → a+quot100 a = case (finiteBitSize a, isSigned a) of+  (64, True)+    | finiteBitSize (0 ∷ Int) == 64 →+        cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int))+  (64, False)+    | finiteBitSize (0 ∷ Word) == 64 →+        cast $$(quoteQuot (100 ∷ Word))+  (32, True) → cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int32))+  (32, False) → cast $$(quoteQuot (100 ∷ Word32))+  (16, True) → cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int16))+  (16, False) → cast $$(quoteQuot (100 ∷ Word16))+  (8, True) → cast $$(quoteAST $ assumeNonNegArg $ astQuot (100 ∷ Int8))+  (8, False) → cast $$(quoteQuot (100 ∷ Word8))+  _ → a `quot` 100+  where+    cast ∷ (Integral a, Integral b) ⇒ (b → b) → a+    cast f = fromIntegral (f (fromIntegral a))+{-# INLINEABLE quot100 #-}++quotBillion ∷ (Integral a, FiniteBits a) ⇒ a → a+#ifdef aarch64_HOST_ARCH+quotBillion a = a `quot` 1e9+#else+quotBillion a = case (finiteBitSize a, isSigned a) of+  (64, True)+    | finiteBitSize (0 :: Int) == 64+    → cast $$(quoteAST $ assumeNonNegArg $ astQuot (1e9 :: Int))+  (64, False)+    | finiteBitSize (0 :: Word) == 64+    → cast $$(quoteQuot (1e9 :: Word))+  (32, True)  → cast $$(quoteAST $ assumeNonNegArg $ astQuot (1e9 :: Int32))+  (32, False) → cast $$(quoteQuot (1e9 :: Word32))+  _ → a `quot` 1e9+  where+    cast :: (Integral a, Integral b) => (b → b) → a+    cast f = fromIntegral (f (fromIntegral a))+#endif+{-# INLINEABLE quotBillion #-}
+ src/Data/Text/Builder/Linear/Dec/Unbounded.hs view
@@ -0,0 +1,314 @@+-- |+-- Copyright:   (c) 2024 Pierre Le Marre+-- Licence:     BSD3+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>+module Data.Text.Builder.Linear.Dec.Unbounded (+  (|>$$),+  ($$<|),+  -- prependUnboundedDecimal,+  -- Strategy (..),+)+where++import Data.Bits (Bits (..), FiniteBits (..))+import Data.Text.Array qualified as A+import Data.Word (Word64)+import GHC.Exts (+  Int (..),+  Int#,+  State#,+  Word (..),+  Word#,+  word2Int#,+  (-#),+ )+import GHC.Num.BigNat qualified as BN+import GHC.Num.Integer qualified as I+import GHC.Num.Natural qualified as N+import GHC.Ptr (plusPtr)+import GHC.ST (ST (..))++import Data.Text.Builder.Linear.Array (unsafeReplicate)+import Data.Text.Builder.Linear.Core (Buffer)+import Data.Text.Builder.Linear.Dec.Bounded (digits, maxDecLen, quotRem100)+import Data.Text.Builder.Linear.Dec.Bounded qualified as Bounded+import Data.Text.Builder.Linear.Internal (appendBounded', prependBounded')++--------------------------------------------------------------------------------+-- Append+--------------------------------------------------------------------------------++-- | Append the decimal representation of an /unbounded/ integral number.+--+-- @since 0.1.3+(|>$$) ∷ Integral a ⇒ Buffer ⊸ a → Buffer++infixl 6 |>$$+buffer |>$$ n = case toInteger n of+  !n' →+    appendBounded'+      (maxIntegerDecLen n')+      (unsafeAppendDec n')+      buffer+{-# INLINEABLE (|>$$) #-}++-- • For small 'Integers', `unsafeAppendDec`+-- • For 'BigNat's, use a buffer with `unsafePrependUnboundedDec`, then copy it.+--+-- For *bounded* integers we used the exact size of the decimal representation to+-- compute the offset from which we can use the prepend action to actually append.+--+-- But the exact size of an (unbounded) 'Integer' could be expensive to compute.+-- So it is faster to use a buffer and then copy it.+unsafeAppendDec+  ∷ ∀ s x+   . Integer+  → ((A.MArray s → Int → ST s Int) → ST s x)+  → ((A.MArray s → Int → ST s Int) → ST s x)+  → ST s x+unsafeAppendDec n = case n of+  I.IS i# → \append _ → append (\marr off → Bounded.unsafeAppendDec marr off (I# i#))+  _ → \_ prepend → prepend (\marr off → unsafePrependDec marr off n)+{-# INLINEABLE unsafeAppendDec #-}++--------------------------------------------------------------------------------+-- Prepend+--------------------------------------------------------------------------------++-- | Prepend the decimal representation of an /unbounded/ integral number.+--+-- @since 0.1.3+($$<|) ∷ Integral a ⇒ a → Buffer ⊸ Buffer++infixr 6 $$<|+n $$<| buffer = case toInteger n of+  !n' →+    prependBounded'+      (maxIntegerDecLen n')+      (\dst dstOff → unsafePrependDec dst dstOff n')+      buffer+{-# INLINEABLE ($$<|) #-}++unsafePrependDec ∷ ∀ s. A.MArray s → Int → Integer → ST s Int+unsafePrependDec marr off@(I# off#) n = case n of+  I.IS i# → Bounded.unsafePrependDec marr off (I# i#)+  _ → unsafePrependBigNatDec marr (off# -# 1#) (integerToBigNat# n) >>= prependSign+    where+      prependSign !off' =+        if n < 0+          then do+            A.unsafeWrite marr (off' - 1) 0x2d -- '-'+            pure (off - off' + 1)+          else pure (off - off')+{-# INLINEABLE unsafePrependDec #-}++type DigitsWriter s = Int# → BN.BigNat# → ST s Int++-- Use the fastest writer depending on the BigNat size+unsafePrependBigNatDec ∷ ∀ s. A.MArray s → DigitsWriter s+unsafePrependBigNatDec marr !off0 !n0+  | BN.bigNatSize n0 < hugeSizeThreshold = prependSmallNat marr off0 n0+  | otherwise = prependHugeNat marr off0 n0+  where+    hugeSizeThreshold ∷ Word+    hugeSizeThreshold = 80++-- Writer for “small” 'BigNat's.+--+-- Divide repeatedly by poweredBase.+prependSmallNat ∷ ∀ s. A.MArray s → DigitsWriter s+prependSmallNat marr = go+  where+    !(# power, poweredBase, _poweredBase² #) = selectPower (# #)++    go ∷ DigitsWriter s+    go !o1 !n = case n `BN.bigNatQuotRemWord#` poweredBase of+      (# q, r #) → do+        !o2 ← unsafePrependWordDec marr (I# o1) (W# r)+        if BN.bigNatIsZero q+          then pure o2+          else do+            let !o3 = o1 -# (word2Int# power -# 1#)+            padWithZeros marr (I# o3) (o2 - I# o3)+            go (o3 -# 1#) q++-- Use the raw state in order to avoid boxed Int in `scaleWriter`+type DigitsWriter# s = Int# → BN.BigNat# → State# s → (# State# s, Int# #)++-- Writer for “huge” 'BigNat's.+--+-- Algorithm used in bytestring-0.12.1 (simplified):+--+-- 1. Find k0 = min k such that pow10 ^ (2 ^ (k + 1)) > n0+-- 2. Set k to k0 and n to n0+-- 3. Set (q, r) = n `quotRem` (pow10 ^ (2 ^ k))+-- 4. if k = 0, then write decimal representation of q and r+--    else repeat recursively 3 and 4 with n = {q,r} and k = k - 1+prependHugeNat ∷ ∀ s. A.MArray s → DigitsWriter s+prependHugeNat marr off n = ST $ \s1 →+  case go prependTiny# poweredBase² off n s1 of+    (# s2, off'# #) → (# s2, I# off'# #)+  where+    !(# power, poweredBase, poweredBase² #) = selectPower (# #)++    go ∷ (Bool → DigitsWriter# s) → BN.BigNat# → DigitsWriter# s+    go !write !pow10 !o !n# =+      if BN.bigNatLt n# pow10+        then write True o n#+        else go (scaleWriter write pow10) (BN.bigNatMul pow10 pow10) o n#++    scaleWriter ∷ (Bool → DigitsWriter# s) → BN.BigNat# → Bool → DigitsWriter# s+    scaleWriter !write !pow10 = \ !high !o1 !n# s1 →+      case BN.bigNatQuotRem# n# pow10 of+        (# q, r #)+          | high && BN.bigNatIsZero q → write high o1 r s1+          | otherwise → case write False o1 r s1 of+              (# s2, o2 #) → write high (o2 -# 1#) q s2++    prependTiny# ∷ Bool → DigitsWriter# s+    prependTiny# !high !o1 !n# = case prependTiny high o1 n# of+      ST f → \s1 → case f s1 of+        (# s2, I# o2 #) → (# s2, o2 #)++    -- Use ST instead of raw state as the utils functions do.+    -- `prependTiny` must inline to leave no boxing/unboxing roundtrip.+    {-# INLINE prependTiny #-}+    prependTiny ∷ Bool → DigitsWriter s+    prependTiny !high !o1 !n# =+      case BN.bigNatQuotRemWord# n# poweredBase of+        (# q, r #) → do+          !o2 ← unsafePrependWordDec marr (I# o1) (W# r)+          if high && BN.bigNatIsZero q+            then pure o2+            else do+              let !o3 = I# o1 - (fromIntegral (W# power) - 1)+              padWithZeros marr o3 (o2 - o3)+              !o4 ← unsafePrependWordDec marr (o3 - 1) (BN.bigNatToWord q)+              if high+                then pure o4+                else do+                  let !o5 = o3 - fromIntegral (W# power)+                  padWithZeros marr o5 (o4 - o5)+                  pure o5++--------------------------------------------------------------------------------+-- Prepend word+--------------------------------------------------------------------------------++unsafePrependWordDec ∷ ∀ s. A.MArray s → Int → Word → ST s Int+unsafePrependWordDec = f+  where+    f marr !o !k+      | k >= 10 = do+          let (q, r) = quotRem100 k+          A.copyFromPointer marr (o - 1) (digits `plusPtr` (fromIntegral r `shiftL` 1)) 2+          if k < 100 then pure (o - 1) else f marr (o - 2) q+      | otherwise = do+          A.unsafeWrite marr o (fromIntegral (0x30 + k))+          pure o++--------------------------------------------------------------------------------+-- Utils+--------------------------------------------------------------------------------++maxIntegerDecLen ∷ Integer → Int+maxIntegerDecLen a = case a of+  I.IS i# → maxDecLen (I# i#)+  I.IP n# → maxBitNatDecLen n#+  I.IN n# → 1 + maxBitNatDecLen n#+{-# INLINEABLE maxIntegerDecLen #-}++-- | ceiling (fbs a * logBase 10 2) < ceiling (fbs a * 5 / 16) < 1 + floor (fbs a * 5 / 16)+--+-- We approximate @fbs a@ to @bigNatSize a * word_size@.+maxBitNatDecLen ∷ BN.BigNat# → Int+maxBitNatDecLen n#+  -- This can overflow in theory, but in practice it would overflow for a BigNat#+  -- of at least:+  --+  -- • On 32 bits platform: 6.4 GiB, out of max 4 GiB RAM+  --   → BN.bigNatSize n# = 214748364 =+  --       (maxBound @Int32 - 1) `div` fromIntegral (shiftR (finiteBitSize @Word32 0 * 5) 4)+  -- • On 64 bits platform: 3276 PiB+  --   → BN.bigNatSize n# = 461168601842738790 =+  --       (maxBound @Int64 - 1) `div` fromIntegral (shiftR (finiteBitSize @Word64 0 * 5) 4)+  --+  -- These thresholds are too big to be realistic (32 bits: more than available RAM, 64+  -- bits: integer size in petabytes), so it is perfectly reasonable to have no+  -- special handling of overflow here.++  -- Word bit size is multiple of 16 (e.g. 32 and 64 bits arch)+  | rem (finiteBitSize @Word 0) 16 == 0 =+      1 + fromIntegral (BN.bigNatSize n# * shiftR (fromIntegral (finiteBitSize @Word 0) * 5) 4)+  -- Other cases (non-standard arch)+  | otherwise =+      1+        + fromIntegral @Word64+          ( (fromIntegral (BN.bigNatSize n#) * fromIntegral (finiteBitSize @Word 0) * 5)+              `shiftR` 4+          )+{-# INLINEABLE maxBitNatDecLen #-}++integerToBigNat# ∷ Integer → BN.BigNat#+integerToBigNat# n = case I.integerToBigNatSign# n of+  (# _, n# #) → n#+{-# INLINE integerToBigNat# #-}++-- Maximal power of 10 fitting into a 'Word':+-- • 10 ^ 9  for 32 bit words  (32 * log 2 / log 10 ≈  9.63)+-- • 10 ^ 19 for 64 bit words  (64 * log 2 / log 10 ≈ 19.27)+--+-- Why (# #)? We can't have top-level unlifted bindings+-- (see: https://gitlab.haskell.org/ghc/ghc/-/issues/17521). So we use a function+-- that take an empty argument (# #) that will be discarded at compile time.+selectPower ∷ (# #) → (# Word#, Word#, BN.BigNat# #)+selectPower _ = case finiteBitSize @Word 0 of+  64 → (# 19##, 10000000000000000000##, N.naturalToBigNat# tenPower38 #)+  -- Not 64 bits: assume 32 bits+  _ → (# 9##, 1000000000##, N.naturalToBigNat# tenPower18 #)++-- NOTE: ensure to not inline the following numbers, in order to avoid allocations.++tenPower18 ∷ N.Natural+tenPower18 = 1e18+{-# NOINLINE tenPower18 #-}++tenPower38 ∷ N.Natural+tenPower38 = 1e38+{-# NOINLINE tenPower38 #-}++padWithZeros ∷ ∀ s. A.MArray s → Int → Int → ST s ()+padWithZeros marr off count = unsafeReplicate marr off count 0x30+{-# INLINE padWithZeros #-}++--------------------------------------------------------------------------------+-- For testing purpose only+--------------------------------------------------------------------------------++-- data Strategy = SmallOnly | HugeOnly++-- prependUnboundedDecimal ∷ Integral a ⇒ Strategy → a → Buffer ⊸ Buffer+-- prependUnboundedDecimal strategy n buffer = case toInteger n of+--   !n' →+--     prependBounded'+--       (maxIntegerDecLen n')+--       (\dst dstOff → unsafePrependDec' strategy dst dstOff n')+--       buffer++-- unsafePrependDec' ∷ ∀ s. Strategy → A.MArray s → Int → Integer → ST s Int+-- unsafePrependDec' s marr off@(I# off#) n' = case n' of+--   I.IS i# → Bounded.unsafePrependDec marr off (I# i#)+--   _ → unsafePrependBigNatDec' s marr (off# -# 1#) (integerToBigNat# n') >>= prependSign+--     where+--       prependSign !off' =+--         if n' < 0+--           then do+--             A.unsafeWrite marr (off' - 1) 0x2d -- '-'+--             pure (off - off' + 1)+--           else pure (off - off')+-- {-# INLINEABLE unsafePrependDec' #-}++-- unsafePrependBigNatDec' ∷ ∀ s. Strategy → A.MArray s → DigitsWriter s+-- unsafePrependBigNatDec' strategy marr !off0 !n0 = case strategy of+--   SmallOnly → prependSmallNat marr off0 n0+--   HugeOnly → prependHugeNat marr off0 n0
src/Data/Text/Builder/Linear/Double.hs view
@@ -19,7 +19,13 @@  import Data.Text.Builder.Linear.Core --- | Append double.+-- | Append the decimal representation of a 'Double'.+--+-- >>> runBuffer (\b -> b |>% 123.456)+-- "123.456"+--+-- >>> runBuffer (\b -> b |>% 1.23e7)+-- "1.23e7" (|>%) ∷ Buffer ⊸ Double → Buffer  infixl 6 |>%@@ -29,7 +35,7 @@     (\dst dstOff → unsafeAppendDouble dst dstOff x)     buffer --- | Prepend double.+-- | Prepend the decimal representation of a 'Double'. (%<|) ∷ Double → Buffer ⊸ Buffer  infixr 6 %<|
src/Data/Text/Builder/Linear/Hex.hs view
@@ -15,9 +15,10 @@  import Data.Text.Builder.Linear.Core --- | Append the lower-case hexadecimal represensation of a number.+-- | Append the lower-case hexadecimal representation of a /bounded/ integral+-- number. ----- Negative numbers are interpreted as their corresponding unsigned number, e.g.+-- Negative numbers are interpreted as their corresponding unsigned number: -- -- >>> :set -XOverloadedStrings -XLinearTypes -- >>> import Data.Int (Int8, Int16)@@ -35,9 +36,10 @@     buffer {-# INLINEABLE (|>&) #-} --- | Prepend the lower-case hexadecimal representation of a number.+-- | Prepend the lower-case hexadecimal representation of a /bounded/ integral+-- number. ----- Negative numbers are interpreted as their corresponding unsigned number, e.g.+-- Negative numbers are interpreted as their corresponding unsigned number: -- -- >>> :set -XOverloadedStrings -XLinearTypes -- >>> import Data.Int (Int8, Int16)@@ -92,7 +94,7 @@ -- 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,+-- instead of doing it on every iteration of unsafe{Ap,Pre}pendHex.go, -- but the performance impact is likely negligible. dropNibble ∷ (Integral a, FiniteBits a) ⇒ a → a dropNibble x = case (isSigned x, finiteBitSize x) of
+ src/Data/Text/Builder/Linear/Internal.hs view
@@ -0,0 +1,437 @@+{-# LANGUAGE CPP #-}++-- |+-- Copyright:   (c) 2022 Andrew Lelechenko+--              (c) 2023 Pierre Le Marre+-- Licence:     BSD3+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>+--+-- Internal routines for t'Buffer' manipulations.+module Data.Text.Builder.Linear.Internal (+  -- * Type+  Buffer,++  -- * Basic interface+  runBuffer,+  runBufferBS,+  dupBuffer,+  consumeBuffer,+  eraseBuffer,+  byteSizeOfBuffer,+  lengthOfBuffer,+  dropBuffer,+  takeBuffer,+  newEmptyBuffer,++  -- * Text concatenation+  appendBounded,+  appendExact,+  prependBounded,+  prependBounded',+  appendBounded',+  prependExact,+  (><),+) where++import Data.ByteString.Internal (ByteString (..))+import Data.Text qualified as T+import Data.Text.Array qualified as A+import Data.Text.Internal (Text (..))+import GHC.ForeignPtr (ForeignPtr (..), ForeignPtrContents (..))+import GHC.ST (ST (..), runST)++#if MIN_VERSION_base(4,20,0)+import GHC.Exts (Int (..), Levity (..), RuntimeRep (..), TYPE, byteArrayContents#, plusAddr#, unsafeThawByteArray#, realWorld#)+#else+import GHC.Exts (Int (..), Levity (..), RuntimeRep (..), TYPE, byteArrayContents#, plusAddr#, unsafeCoerce#)+#endif++import Data.Text.Builder.Linear.Array++-- | Internally t'Buffer' is a mutable buffer.+-- If a client gets hold of a variable of type t'Buffer',+-- they'd be able to pass a mutable buffer to concurrent threads.+-- That's why API below is carefully designed to prevent such possibility:+-- clients always work with linear functions t'Buffer' ⊸ t'Buffer' instead+-- and run them on an empty t'Buffer' to extract results.+--+-- In terms of [@linear-base@](https://hackage.haskell.org/package/linear-base)+-- t'Buffer' is [@Consumable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Consumable)+-- (see 'consumeBuffer')+-- and [@Dupable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Dupable)+-- (see 'dupBuffer'),+-- but not [@Movable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Movable).+--+-- >>> :set -XOverloadedStrings -XLinearTypes+-- >>> import Data.Text.Builder.Linear.Buffer+-- >>> runBuffer (\b -> '!' .<| "foo" <| (b |> "bar" |>. '.'))+-- "!foobar."+--+-- Remember: this is a strict builder, so on contrary to "Data.Text.Lazy.Builder"+-- for optimal performance you should use strict left folds instead of lazy right ones.+--+-- t'Buffer' is an unlifted datatype,+-- so you can put it into an unboxed tuple @(# ..., ... #)@,+-- but not into @(..., ...)@.+data Buffer ∷ TYPE ('BoxedRep 'Unlifted) where+  Buffer ∷ {-# UNPACK #-} !Text → Buffer++-- | Unwrap t'Buffer', no-op.+-- Most likely, this is not the function you're looking for+-- and you need 'runBuffer' instead.+unBuffer ∷ Buffer ⊸ Text+unBuffer (Buffer x) = x++-- | Run a linear function on an empty t'Buffer', producing a strict t'Text'.+--+-- 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 -> ...@.+-- 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).+--+-- 'runBuffer' is similar in spirit to mutable arrays API in+-- [@Data.Array.Mutable.Linear@](https://hackage.haskell.org/package/linear-base/docs/Data-Array-Mutable-Linear.html),+-- which provides functions like+-- [@fromList@](https://hackage.haskell.org/package/linear-base/docs/Data-Array-Mutable-Linear.html#v:fromList) ∷ @Movable@ @b@ ⇒ [@a@] → (@Array@ @a@ ⊸ @b@) ⊸ @b@.+-- Here the initial buffer is always empty and @b@ is t'Text'.+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 t'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+  Buffer (Text (A.ByteArray arr) (I# from) len) → BS fp len+    where+      addr# = byteArrayContents# arr `plusAddr#` from+#if MIN_VERSION_base(4,20,0)+      fp = ForeignPtr addr# (PlainPtr (let !(# _, ma #) = unsafeThawByteArray# arr realWorld# in ma))+#else+      fp = ForeignPtr addr# (PlainPtr (unsafeCoerce# arr))+#endif+{-# NOINLINE runBufferBS #-}++shrinkBuffer ∷ Buffer ⊸ Buffer+shrinkBuffer (Buffer (Text arr from len)) = Buffer $ runST $ do+  arrM ← unsafeThaw arr+  A.shrinkM arrM (from + len)+  arr' ← A.unsafeFreeze arrM+  pure $ Text arr' from len++memptyPinned ∷ Text+memptyPinned = runST $ do+  marr ← A.newPinned 0+  arr ← A.unsafeFreeze marr+  pure $ Text arr 0 0++-- | Create an empty t'Buffer'.+--+-- The first t'Buffer' is the input and the second is a new empty t'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` "AAA" `appendJustified` "BBBBBBB")+-- "Test:         AAA     BBBBBBB"+--+-- Note: a previous buffer is necessary in order to create an empty buffer with+-- the same pinnedness.+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)+-- from [@linear-base@](https://hackage.haskell.org/package/linear-base).+--+-- It is a bit tricky to use because of+-- <https://downloads.haskell.org/ghc/9.8.1/docs/users_guide/exts/linear_types.html#limitations current limitations>+-- of linear types with regards to @let@ and @where@. E. g., one cannot write+--+-- > let (# b1, b2 #) = dupBuffer b in ("foo" <| b1) >< (b2 |> "bar")+--+-- Instead write:+--+-- >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples+-- >>> import Data.Text.Builder.Linear.Buffer+-- >>> runBuffer (\b -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar"))+-- "foobar"+--+-- Note the unboxed tuple: t'Buffer' is an unlifted datatype,+-- so it cannot be put into @(..., ...)@.+dupBuffer ∷ Buffer ⊸ (# Buffer, Buffer #)+dupBuffer (Buffer x) = (# Buffer x, Buffer (T.copy x) #)++-- | Consume buffer linearly,+-- similar to+-- [@Consumable@](https://hackage.haskell.org/package/linear-base/docs/Prelude-Linear.html#t:Consumable)+-- from [@linear-base@](https://hackage.haskell.org/package/linear-base).+consumeBuffer ∷ Buffer ⊸ ()+consumeBuffer Buffer {} = ()++-- | Erase buffer's content, replacing it with an empty t'Text'.+eraseBuffer ∷ Buffer ⊸ Buffer+eraseBuffer (Buffer (Text arr _ _)) =+  Buffer (if isPinned arr then memptyPinned else mempty)++-- | Return buffer's size in __bytes__ (not in 'Char's).+-- This could be useful to implement a lazy builder atop of a strict one.+byteSizeOfBuffer ∷ Buffer ⊸ (# Buffer, Word #)+byteSizeOfBuffer (Buffer t@(Text _ _ len)) = (# Buffer t, fromIntegral len #)++-- | Return buffer's length in 'Char's (not in bytes).+-- This could be useful to implement @dropEndBuffer@ and @takeEndBuffer@, e. g.,+--+-- @+-- import Data.Unrestricted.Linear+--+-- dropEndBuffer :: Word -> Buffer %1 -> Buffer+-- 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) #)++-- | Slice t'Buffer' by dropping given number of 'Char's.+dropBuffer ∷ Word → Buffer ⊸ Buffer+dropBuffer nChar (Buffer t@(Text arr off len))+  | nByte < 0 = Buffer (Text arr (off + len) 0)+  | otherwise = Buffer (Text arr (off + nByte) (len - nByte))+  where+    nByte = T.measureOff (fromIntegral nChar) t++-- | Slice t'Buffer' by taking given number of 'Char's.+takeBuffer ∷ Word → Buffer ⊸ Buffer+takeBuffer nChar (Buffer t@(Text arr off _))+  | nByte < 0 = Buffer t+  | otherwise = Buffer (Text arr off nByte)+  where+    nByte = T.measureOff (fromIntegral nChar) t++-- | Low-level routine to append data of unknown size to a t'Buffer'.+appendBounded+  ∷ Int+  -- ^ Upper bound for the number of bytes, written by an action+  → (∀ s. A.MArray s → Int → ST s Int)+  -- ^ Action, which writes bytes __starting__ from the given offset+  -- and returns an actual number of bytes written.+  → Buffer+  ⊸ Buffer+appendBounded maxSrcLen appender (Buffer (Text dst dstOff dstLen)) = Buffer $ runST $ do+  let dstFullLen = sizeofByteArray dst+      newFullLen = dstOff + 2 * (dstLen + maxSrcLen)+  newM ←+    if dstOff + dstLen + maxSrcLen <= dstFullLen+      then unsafeThaw dst+      else do+        tmpM ← (if isPinned dst then A.newPinned else A.new) newFullLen+        A.copyI dstLen tmpM dstOff dst dstOff+        pure tmpM+  srcLen ← appender newM (dstOff + dstLen)+  new ← A.unsafeFreeze newM+  pure $ Text new dstOff (dstLen + srcLen)+{-# INLINE appendBounded #-}++-- | Low-level routine to append data of unknown size to a t'Buffer', giving+-- the action the choice between two strategies.+--+-- See also: 'appendBounded'.+--+-- @since 0.1.3+appendBounded'+  ∷ Int+  -- ^ Upper bound for the number of bytes, written by an action+  → (∀ s x. ((A.MArray s → Int → ST s Int) → ST s x) → ((A.MArray s → Int → ST s Int) → ST s x) → ST s x)+  -- ^ Action, which appends bytes using one of the following strategies:+  --+  -- * writes bytes __starting__ from the given offset, using its first argument,+  -- * writes bytes __finishing__ before the given offset, using its second argument.+  --+  -- The function passed to either argument returns the actual number of bytes written.+  → Buffer+  ⊸ Buffer+appendBounded' maxSrcLen writer (Buffer (Text dst dstOff dstLen)) = Buffer $ runST $ do+  let dstFullLen = sizeofByteArray dst+      newFullLen = dstOff + 2 * (dstLen + maxSrcLen)+  newM ←+    if dstOff + dstLen + maxSrcLen <= dstFullLen+      then unsafeThaw dst+      else do+        tmpM ← (if isPinned dst then A.newPinned else A.new) newFullLen+        A.copyI dstLen tmpM dstOff dst dstOff+        pure tmpM+  let append = \appender → do+        count ← appender newM (dstOff + dstLen)+        pure (dstOff, count)+  -- Action that prepends then copies the result to the final destination, if necessary+  let prepend = \prepender → case dstLen of+        0 → do+          -- Buffer is empty: prepend to final destination+          count ← prepender newM maxSrcLen+          pure (maxSrcLen - count, count)+        _ → do+          -- Require extra buffer + copy to final destination+          let off'+                -- Reuse space before current data (no overlap)+                | dstOff >= maxSrcLen = dstOff+                -- Reuse space after current data (overlap)+                | otherwise = dstOff + dstLen + maxSrcLen+          count ← prepender newM off'+          -- Note: we rely on copyM allowing overlaps+          A.copyM newM (dstOff + dstLen) newM (off' - count) count+          pure (dstOff, count)+  (dstOff', srcLen) ← writer append prepend+  new ← A.unsafeFreeze newM+  pure $ Text new dstOff' (dstLen + srcLen)+{-# INLINE appendBounded' #-}++-- | Low-level routine to append data of known size to a t'Buffer'.+appendExact+  ∷ Int+  -- ^ Exact number of bytes, written by an action+  → (∀ s. A.MArray s → Int → ST s ())+  -- ^ Action, which writes bytes __starting__ from the given offset+  → Buffer+  ⊸ Buffer+appendExact srcLen appender =+  appendBounded+    srcLen+    (\dst dstOff → appender dst dstOff >> pure srcLen)+{-# INLINE appendExact #-}++-- | Low-level routine to prepend data of unknown size to a t'Buffer'.+prependBounded+  ∷ Int+  -- ^ Upper bound for the number of bytes, written by an action+  → (∀ s. A.MArray s → Int → ST s Int)+  -- ^ Action, which writes bytes __finishing__ before the given offset+  -- and returns an actual number of bytes written.+  → (∀ s. A.MArray s → Int → ST s Int)+  -- ^ Action, which writes bytes __starting__ from the given offset+  -- and returns an actual number of bytes written.+  → Buffer+  ⊸ Buffer+prependBounded maxSrcLen prepender appender (Buffer (Text dst dstOff dstLen))+  | maxSrcLen <= dstOff = Buffer $ runST $ do+      newM ← unsafeThaw dst+      srcLen ← prepender newM dstOff+      new ← A.unsafeFreeze newM+      pure $ Text new (dstOff - srcLen) (srcLen + dstLen)+  | otherwise = Buffer $ runST $ do+      let dstFullLen = sizeofByteArray dst+          newOff = dstLen + maxSrcLen+          newFullLen = 2 * newOff + (dstFullLen - dstOff - dstLen)+      newM ← (if isPinned dst then A.newPinned else A.new) newFullLen+      srcLen ← appender newM newOff+      A.copyI dstLen newM (newOff + srcLen) dst dstOff+      new ← A.unsafeFreeze newM+      pure $ Text new newOff (dstLen + srcLen)+{-# INLINE prependBounded #-}++-- | Low-level routine to prepend data of unknown size to a t'Buffer'.+--+-- Contrary to 'prependBounded', only use a prepend action.+--+-- @since 0.1.3+prependBounded'+  ∷ Int+  -- ^ Upper bound for the number of bytes, written by an action+  → (∀ s. A.MArray s → Int → ST s Int)+  -- ^ Action, which writes bytes __finishing__ before the given offset+  -- and returns an actual number of bytes written.+  → Buffer+  ⊸ Buffer+prependBounded' maxSrcLen prepender (Buffer (Text dst dstOff dstLen))+  | maxSrcLen <= dstOff = Buffer $ runST $ do+      newM ← unsafeThaw dst+      srcLen ← prepender newM dstOff+      new ← A.unsafeFreeze newM+      pure $ Text new (dstOff - srcLen) (srcLen + dstLen)+  | otherwise = Buffer $ runST $ do+      let dstFullLen = sizeofByteArray dst+          off = dstLen + 2 * maxSrcLen+          newFullLen = off + (dstFullLen - dstOff)+      newM ← (if isPinned dst then A.newPinned else A.new) newFullLen+      srcLen ← prepender newM off+      A.copyI dstLen newM off dst dstOff+      new ← A.unsafeFreeze newM+      pure $ Text new (off - srcLen) (dstLen + srcLen)+{-# INLINE prependBounded' #-}++-- | Low-level routine to append data of known size to a t'Buffer'.+prependExact+  ∷ Int+  -- ^ Exact number of bytes, written by an action+  → (∀ s. A.MArray s → Int → ST s ())+  -- ^ Action, which writes bytes __starting__ from the given offset+  → Buffer+  ⊸ Buffer+prependExact srcLen appender =+  prependBounded+    srcLen+    (\dst dstOff → appender dst (dstOff - srcLen) >> pure srcLen)+    (\dst dstOff → appender dst dstOff >> pure srcLen)+{-# INLINE prependExact #-}++-- | Concatenate two t'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 -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar"))+-- "foobar"+(><) ∷ Buffer ⊸ Buffer ⊸ Buffer++infix 6 ><+Buffer (Text left leftOff leftLen) >< Buffer (Text right rightOff rightLen) = Buffer $ runST $ do+  let leftFullLen = sizeofByteArray left+      rightFullLen = sizeofByteArray right+      canCopyToLeft = leftOff + leftLen + rightLen <= leftFullLen+      canCopyToRight = leftLen <= rightOff+      shouldCopyToLeft = canCopyToLeft && (not canCopyToRight || leftLen >= rightLen)+  if shouldCopyToLeft+    then do+      newM ← unsafeThaw left+      A.copyI rightLen newM (leftOff + leftLen) right rightOff+      new ← A.unsafeFreeze newM+      pure $ Text new leftOff (leftLen + rightLen)+    else+      if canCopyToRight+        then do+          newM ← unsafeThaw right+          A.copyI leftLen newM (rightOff - leftLen) left leftOff+          new ← A.unsafeFreeze newM+          pure $ Text new (rightOff - leftLen) (leftLen + rightLen)+        else do+          let fullLen = leftOff + leftLen + rightLen + (rightFullLen - rightOff - rightLen)+          newM ← (if isPinned left || isPinned right then A.newPinned else A.new) fullLen+          A.copyI leftLen newM leftOff left leftOff+          A.copyI rightLen newM (leftOff + leftLen) right rightOff+          new ← A.unsafeFreeze newM+          pure $ Text new leftOff (leftLen + rightLen)
test/Main.hs view
@@ -21,6 +21,7 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Text.Builder.Linear.Buffer+import Data.Text.Builder.Linear.Core (dropBuffer, takeBuffer) import Data.Text.Internal (Text(..)) import Data.Text.Lazy (toStrict) import Data.Text.Lazy.Builder qualified as TB@@ -71,6 +72,8 @@   | PrependDecI Int   | AppendDecI30 (IntN 30)   | PrependDecI30 (IntN 30)+  | AppendDecInteger Integer+  | PrependDecInteger Integer   | AppendDouble Double   | PrependDouble Double   | AppendSpaces Word@@ -98,6 +101,8 @@     , PrependDecI   <$> arbitraryBoundedIntegral     , AppendDecI30  <$> arbitraryBoundedIntegral     , PrependDecI30 <$> arbitraryBoundedIntegral+    , AppendDecInteger <$> arbitraryInteger+    , PrependDecInteger <$> arbitraryInteger     , pure $ HexWord minBound minBound minBound minBound     , pure $ HexWord maxBound maxBound maxBound maxBound     , pure $ HexInt minBound minBound minBound minBound minBound minBound minBound@@ -116,6 +121,9 @@     where       arbitraryCharCount = chooseBoundedIntegral (0, 6)       arbitraryTotalLength = chooseBoundedIntegral (3, 20)+      arbitraryInteger = chooseInteger+        ( fromIntegral @Int minBound ^ (3 :: Word)+        , fromIntegral @Int maxBound ^ (3 :: Word) )    shrink = genericShrink @@ -165,6 +173,8 @@     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 (AppendDecInteger  x) = b <> toStrict (toLazyText (decimal x))+    go b (PrependDecInteger 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 ' ')@@ -177,12 +187,7 @@       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))+    hexadecimalSW (SomeWordN x) = hexadecimal x      intersperseText ∷ [TB.Builder] → Text     intersperseText bs =@@ -216,6 +221,8 @@     go b (PrependDecI    x) = x $<| b     go b (AppendDecI30   x) = b |>$ x     go b (PrependDecI30  x) = x $<| b+    go b (AppendDecInteger  x) = b |>$$ x+    go b (PrependDecInteger x) = x $$<| b     go b (AppendDouble   x) = b |>% x     go b (PrependDouble  x) = x %<| b     go b (AppendSpaces   n) = b |>… n@@ -230,8 +237,26 @@   , testProperty "bytestring builder" prop5   , testProperty "CSE 1" prop6   , testProperty "CSE 2" prop7+  , testProperty "unbounded integers" prop8+  , testProperty "dropBuffer" propDropBuffer+  , testProperty "dropBuffer" propDropBuffer+  , testProperty "takeBuffer" propTakeBuffer   ] +propDropBuffer :: Word → Text → Property+propDropBuffer n xs =+  suff === runBuffer (\b → dropBuffer n (b |> xs)) .&&.+  T.encodeUtf8 suff === runBufferBS (\b → dropBuffer n (b |> xs))+  where+    suff = T.drop (fromIntegral n) xs++propTakeBuffer :: Word → Text → Property+propTakeBuffer n xs =+  pref === runBuffer (\b → takeBuffer n (b |> xs)) .&&.+  T.encodeUtf8 pref === runBufferBS (\b → takeBuffer n (b |> xs))+  where+    pref = T.take (fromIntegral n) xs+ prop1 ∷ [Action] → Property prop1 acts = interpretOnText acts mempty ===   runBuffer (\b → interpretOnBuffer acts b)@@ -274,6 +299,34 @@         !y = runBuffer (\buf -> (buf |>. '_' |>. 'b') |>… 5)     in (x, y) === (T.pack "_a     ", T.pack "_b     ") +prop8 ∷ Property+prop8 =+  conjoin+    [ check 0+    , check 1e18+    , check 1e19+    , check 1e20+    , check 1e50+    , check 1e100+    , check (10 ^ (400 ∷ Word))+    , check (10 ^ (600 ∷ Word))+    , check (10 ^ (1000 ∷ Word))+    , check (toInteger @Word maxBound)+    , check (toInteger @Word maxBound + 1)+    , check (negate (toInteger @Word maxBound))+    , check (negate (toInteger @Word maxBound + 1))+    , check (toInteger @Word maxBound ^ (2 ∷ Word))+    , check (toInteger @Word maxBound ^ (20 ∷ Word))+    , check (toInteger @Word maxBound ^ (40 ∷ Word))+    ]+  where+    check ∷ Integer → Property+    check i =+      decimalText i === runBuffer (i $$<|)+        .&&.+      decimalText i === runBuffer (|>$$ i)++    decimalText = toStrict . toLazyText . decimal -------------------------------------------------------------------------------- -- IntN --------------------------------------------------------------------------------
text-builder-linear.cabal view
@@ -1,12 +1,15 @@ cabal-version:   2.4 name:            text-builder-linear-version:         0.1.2+version:         0.1.4 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.3 ghc ==9.8.1+tested-with:+    ghc ==9.2.8 ghc ==9.4.8 ghc ==9.6.7 ghc ==9.8.4 ghc ==9.10.3+    ghc ==9.12.2 ghc ==9.14.1+ homepage:        https://github.com/Bodigrim/linear-builder synopsis:        Builder for Text and ByteString based on linear types description:@@ -20,7 +23,7 @@  source-repository head     type:     git-    location: git://github.com/Bodigrim/linear-builder.git+    location: git@github.com:Bodigrim/linear-builder.git  library     exposed-modules:@@ -32,9 +35,11 @@     other-modules:         Data.Text.Builder.Linear.Array         Data.Text.Builder.Linear.Char-        Data.Text.Builder.Linear.Dec+        Data.Text.Builder.Linear.Dec.Bounded+        Data.Text.Builder.Linear.Dec.Unbounded         Data.Text.Builder.Linear.Double         Data.Text.Builder.Linear.Hex+        Data.Text.Builder.Linear.Internal      default-language:   GHC2021     default-extensions:@@ -46,6 +51,7 @@         base >=4.16 && <5,         text >=2.0 && <2.2,         bytestring >=0.11 && <0.13,+        ghc-bignum >=1.1 && <2,         quote-quot >=0.2.1 && <0.3  test-suite linear-builder-tests@@ -54,8 +60,8 @@     hs-source-dirs:     test     default-language:   GHC2021     default-extensions:-        DerivingStrategies LinearTypes MagicHash PatternSynonyms-        UnboxedTuples UnicodeSyntax+        DerivingStrategies LinearTypes MagicHash NumDecimals+        PatternSynonyms UnboxedTuples UnicodeSyntax      ghc-options:         -Wall -Wno-orphans -threaded -rtsopts "-with-rtsopts -N"@@ -65,7 +71,7 @@         text,         text-builder-linear,         tasty >=1.4 && <1.6,-        tasty-quickcheck >=0.10 && <0.11+        tasty-quickcheck >=0.10 && <0.12  benchmark linear-builder-bench     type:               exitcode-stdio-1.0@@ -89,7 +95,8 @@         -- 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,+        -- bytestring-strict-builder >=0.4.5 && <0.5,+        -- text-builder >=1.0 && <1.1,+        -- text-builder-dev >=0.4 && <0.5,         tasty,-        tasty-bench >=0.3.2 && <0.4+        tasty-bench >=0.4 && <0.6