text-builder-linear 0.1.2 → 0.1.4
raw patch · 20 files changed
Files
- README.md +76/−158
- bench/BenchChar.hs +7/−163
- bench/BenchDecimal.hs +4/−4
- bench/BenchDouble.hs +5/−4
- bench/BenchHexadecimal.hs +14/−22
- bench/BenchText.hs +4/−4
- changelog.md +9/−0
- src/Data/Text/Builder/Linear.hs +47/−14
- src/Data/Text/Builder/Linear/Array.hs +4/−5
- src/Data/Text/Builder/Linear/Buffer.hs +20/−13
- src/Data/Text/Builder/Linear/Char.hs +26/−6
- src/Data/Text/Builder/Linear/Core.hs +2/−313
- src/Data/Text/Builder/Linear/Dec.hs +0/−172
- src/Data/Text/Builder/Linear/Dec/Bounded.hs +180/−0
- src/Data/Text/Builder/Linear/Dec/Unbounded.hs +314/−0
- src/Data/Text/Builder/Linear/Double.hs +8/−2
- src/Data/Text/Builder/Linear/Hex.hs +7/−5
- src/Data/Text/Builder/Linear/Internal.hs +437/−0
- test/Main.hs +59/−6
- text-builder-linear.cabal +17/−10
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