unicode-collation 0.1.3.6 → 0.1.3.7
raw patch · 7 files changed
+334/−49 lines, 7 filesdep +deepseqdep ~basedep ~text
Dependencies added: deepseq
Dependency ranges changed: base, text
Files
- CHANGELOG.md +50/−0
- benchmark/Main.hs +29/−11
- benchmark/SortKey.hs +107/−0
- src/Text/Collate.hs +9/−4
- src/Text/Collate/Collation.hs +31/−29
- test/unit.hs +86/−3
- unicode-collation.cabal +22/−2
CHANGELOG.md view
@@ -2,6 +2,56 @@ `unicode-collation` uses [PVP Versioning](https://pvp.haskell.org). +## 0.1.3.7++ * Docs: recommend `sortOn (sortKey c)` over `sortBy (collate c)`.+ `collate` recomputes the sort key of each argument on every+ comparison, whereas `sortOn` computes each key only once. Update+ the headline example and guidance accordingly.++ * Replace IntSet lookups with range checks in implicit weight calculation.+ `calculateImplicitWeight` previously tested CJK/ideograph+ membership using large IntSets defined in its where-clause.+ Since the data is essentially contiguous ranges, replace the+ IntSets with simple range-comparison predicates. This removes+ is ~25% faster on CJK-heavy sorting at -O1. Also drop a+ duplicate range, remove the now-unused Data.IntSet import,+ and correct a misleading (harmless, due to Word16 truncation)+ 0x7FFFF mask to 0x7FFF in the fallback case.++ * Test: guard implicit-weight ideograph ranges against data drift.+ The CJK/Tangut/Nushu/Khitan ranges in Text.Collate.Collation are+ hand-coded and decoupled from the shipped Unicode data. Add a test+ that enumerates every code point the data marks as an ideograph+ (from the @implicitweights directives in allkeys.txt and the CJK+ Ideograph First/Last markers in UnicodeData.txt) and asserts each+ receives an ideographic implicit primary weight (< 0xFBC0) rather+ than the generic unassigned bucket. A future data bump that does+ not update the ranges will now fail, naming the uncovered points.++ * Benchmarks: force full sorts with `nf` instead of `whnf`.+ `whnf` only evaluated the result list to its first cons cell, so the+ benchmarks measured finding the minimum rather than a full sort. Using+ `nf` forces the entire sorted list, giving a representative measurement.++ * Add `bench-sortkey` benchmark comparing `collate` with `sortKey`.+ This self-contained benchmark (no text-icu/QuickCheck deps) measures+ `sortBy (collate c)` against `sortOn (sortKey c)` across several input+ shapes, demonstrating the speedup from computing each sort key once.++ * Add an `icu-benchmark` flag (default off) guarding the existing+ text-icu benchmark, so the new benchmark can be built without the ICU+ C library.++ * Add a CJK-focused benchmark exercising the implicit weight path.++ * Add flake.nix.++ * CI: Support GHC 9.10 (David Binder), 9.12 (Austin Ziegler), and+ 9.14 (Li-yao Xia).++ * Add cabal.project.+ ## 0.1.3.6 * Update to build with GHC 9.8 (Laurent P. René de Cotret).
benchmark/Main.hs view
@@ -19,10 +19,13 @@ (randomTexts :: [Text]) <- generate (infiniteListOf arbitrary) (randomLatinStrings :: [String]) <- generate (infiniteListOf (listOf (elements latinChars)))+ (randomCJKStrings :: [String]) <-+ generate (infiniteListOf (listOf (elements cjkChars))) (randomAsciiTexts :: [Text]) <- generate (infiniteListOf (arbitrary `suchThat` T.all isAscii)) let tenThousand = take 10000 randomTexts let tenThousandLatin = map T.pack $ take 10000 randomLatinStrings+ let tenThousandCJK = map T.pack $ take 10000 randomCJKStrings let tenThousandLatinNFD = map (T.pack . map chr . toNFD . map ord . T.unpack) tenThousandLatin let tenThousandString = map T.unpack tenThousand@@ -33,28 +36,43 @@ let collateString = collateWithUnpacker (collatorFor "en") id defaultMain [ bench "sort a list of 10000 random Texts (en)"- (whnf (sortBy (collate (collatorFor "en"))) tenThousand)+ (nf (sortBy (collate (collatorFor "en"))) tenThousand) , bench "sort same list with text-icu (en)"- (whnf (sortBy (ICU.collate (icuCollator "en"))) tenThousand)+ (nf (sortBy (ICU.collate (icuCollator "en"))) tenThousand) , bench "sort a list of 10000 Texts (composed latin) (en)"- (whnf (sortBy (collate (collatorFor "en"))) tenThousandLatin)+ (nf (sortBy (collate (collatorFor "en"))) tenThousandLatin) , bench "sort same list with text-icu (en)"- (whnf (sortBy (ICU.collate (icuCollator "en"))) tenThousandLatin)+ (nf (sortBy (ICU.collate (icuCollator "en"))) tenThousandLatin) , bench "sort same list but pre-normalized (en-u-kk-false)"- (whnf (sortBy (collate (collatorFor "en-u-kk-false"))) tenThousandLatinNFD)+ (nf (sortBy (collate (collatorFor "en-u-kk-false"))) tenThousandLatinNFD)+ , bench "sort a list of 10000 CJK Texts (en, implicit weights)"+ (nf (sortBy (collate (collatorFor "en"))) tenThousandCJK)+ , bench "sort same CJK list with text-icu (en)"+ (nf (sortBy (ICU.collate (icuCollator "en"))) tenThousandCJK) , bench "sort a list of 10000 ASCII Texts (en)"- (whnf (sortBy (collate (collatorFor "en"))) tenThousandAscii)+ (nf (sortBy (collate (collatorFor "en"))) tenThousandAscii) , bench "sort same list with text-icu (en)"- (whnf (sortBy (ICU.collate (icuCollator "en"))) tenThousandAscii)+ (nf (sortBy (ICU.collate (icuCollator "en"))) tenThousandAscii) , bench "sort a list of 10000 random Texts that agree in first 32 chars"- (whnf (sortBy (collate (collatorFor "en"))) tenThousandLong)+ (nf (sortBy (collate (collatorFor "en"))) tenThousandLong) , bench "sort same list with text-icu (en)"- (whnf (sortBy (ICU.collate (icuCollator "en"))) tenThousandLong)+ (nf (sortBy (ICU.collate (icuCollator "en"))) tenThousandLong) , bench "sort a list of 10000 identical Texts (en)"- (whnf (sortBy collateString) (replicate 10000 "ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔ"))+ (nf (sortBy collateString) (replicate 10000 "ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔ")) , bench "sort a list of 10000 random Strings (en)"- (whnf (sortBy collateString) tenThousandString)+ (nf (sortBy collateString) tenThousandString) ]++-- A mix of CJK ideographs, all of which are assigned implicit weights+-- (they are not listed individually in the DUCET), so this exercises+-- 'calculateImplicitWeight'. Includes the "Core Han" path (CJK Unified+-- Ideographs, BMP) as well as the "All Other Han Unified" path (Extension A+-- and a sample of supplementary Extension B).+cjkChars :: [Char]+cjkChars = map chr $+ [0x4E00..0x9FFF] -- CJK Unified Ideographs (BMP, Core Han)+ ++ [0x3400..0x4DBF] -- CJK Extension A+ ++ [0x20000..0x20FFF] -- sample of CJK Extension B (supplementary) latinChars :: [Char] latinChars = "ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿ‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…"
+ benchmark/SortKey.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Benchmark comparing two ways of sorting with a 'Collator':+--+-- * naive: @sortBy (collate coll)@ -- recomputes both sort keys+-- on every comparison+-- * keyed: @sortOn (sortKey coll)@ -- computes each sort key once+--+-- Inputs are generated with a self-contained xorshift PRNG so this target+-- depends only on the library, text, deepseq and tasty-bench (no text-icu /+-- QuickCheck). Results are forced with 'nf' so the whole sort is measured.+module Main (main) where++import Test.Tasty.Bench+import Control.DeepSeq (force)+import Control.Exception (evaluate)+import Data.Bits (xor, shiftL, shiftR)+import Data.Char (chr)+import Data.List (sortBy, sortOn)+import Data.Word (Word64)+import Data.Text (Text)+import qualified Data.Text as T+import Text.Collate++-- ---------------------------------------------------------------------------+-- A tiny deterministic PRNG (xorshift64), so benchmarks are reproducible+-- and we avoid a QuickCheck dependency.++xorshift :: Word64 -> Word64+xorshift s0 =+ let s1 = s0 `xor` (s0 `shiftL` 13)+ s2 = s1 `xor` (s1 `shiftR` 7)+ in s2 `xor` (s2 `shiftL` 17)++randoms :: Word64 -> [Word64]+randoms = drop 1 . iterate xorshift++-- Map a Word64 to a Char in [lo,hi] (inclusive code point range).+type CharGen = Word64 -> Char++rangeGen :: Int -> Int -> CharGen+rangeGen lo hi w = chr (lo + fromIntegral (w `mod` fromIntegral (hi - lo + 1)))++-- Build n Texts. Each text has a length drawn from [minLen,maxLen] and+-- characters produced by the given CharGen.+buildTexts :: Int -> (Int, Int) -> CharGen -> [Word64] -> [Text]+buildTexts 0 _ _ _ = []+buildTexts n (lo, hi) g ws0 =+ case ws0 of+ [] -> []+ (lenW : ws) ->+ let len = lo + fromIntegral (lenW `mod` fromIntegral (hi - lo + 1))+ (chunk, ws') = splitAt len ws+ txt = T.pack (map g chunk)+ in txt : buildTexts (n - 1) (lo, hi) g ws'++-- A text whose first 25 chars are a shared prefix (worst case for the naive+-- sort: every comparison must scan past the identical prefix before it can+-- decide, forcing nearly the whole sort key each time).+withSharedPrefix :: Text -> Text+withSharedPrefix = ("The quick brown fox jumps" <>)++-- ---------------------------------------------------------------------------+-- The two sorting strategies.++naiveSort :: Collator -> [Text] -> [Text]+naiveSort coll = sortBy (collate coll)++keyedSort :: Collator -> [Text] -> [Text]+keyedSort coll = sortOn (sortKey coll)++-- ---------------------------------------------------------------------------++main :: IO ()+main = do+ let coll = collatorFor "en"+ n = 10000++ -- Four data shapes, each a forced list of 10000 Texts.+ ascii <- evaluate $ force $+ buildTexts n (1, 20) (rangeGen 0x61 0x7A) (randoms 0x1234)+ latin <- evaluate $ force $ -- Latin Extended Additional: need NFD+ buildTexts n (1, 20) (rangeGen 0x1E00 0x1EFF) (randoms 0x5678)+ cjk <- evaluate $ force $ -- implicit weights path+ buildTexts n (1, 20) (rangeGen 0x4E00 0x9FFF) (randoms 0x9ABC)+ long <- evaluate $ force $ map withSharedPrefix $+ buildTexts n (1, 20) (rangeGen 0x61 0x7A) (randoms 0xDEF0)++ defaultMain+ [ bgroup "ascii"+ [ bench "naive (collate)" $ nf (naiveSort coll) ascii+ , bench "keyed (sortKey)" $ nf (keyedSort coll) ascii+ ]+ , bgroup "latin (needs NFD)"+ [ bench "naive (collate)" $ nf (naiveSort coll) latin+ , bench "keyed (sortKey)" $ nf (keyedSort coll) latin+ ]+ , bgroup "cjk (implicit weights)"+ [ bench "naive (collate)" $ nf (naiveSort coll) cjk+ , bench "keyed (sortKey)" $ nf (keyedSort coll) cjk+ ]+ , bgroup "shared-prefix (worst case)"+ [ bench "naive (collate)" $ nf (naiveSort coll) long+ , bench "keyed (sortKey)" $ nf (keyedSort coll) long+ ]+ ]
src/Text/Collate.hs view
@@ -11,9 +11,9 @@ instance of 'Collator' (together with the @OverloadedStrings@ extension): ->>> import Data.List (sortBy)+>>> import Data.List (sortOn) >>> import qualified Data.Text.IO as T->>> mapM_ T.putStrLn $ sortBy (collate "en-US") ["𝒶bc","abC","𝕒bc","Abc","abç","äbc"]+>>> mapM_ T.putStrLn $ sortOn (sortKey "en-US") ["𝒶bc","abC","𝕒bc","Abc","abç","äbc"] abC 𝒶bc 𝕒bc@@ -34,8 +34,13 @@ 𝕒bc A 'Collator' provides a function 'collate' that compares two texts,-and a function 'sortKey' that returns the sort key. Most users will-just need 'collate'.+and a function 'sortKey' that returns the sort key. Use 'collate'+when you only need to compare two values. To sort a list, prefer+@sortOn (sortKey collator)@ (as above) over @sortBy (collate collator)@:+'collate' recomputes the sort key of each argument on every+comparison, whereas 'sortOn' (from "Data.List") computes each key only+once. This is substantially faster when sorting large lists (in+benchmarks, roughly 3-5x). >>> let de = collatorFor "de" >>> let se = collatorFor "se"
src/Text/Collate/Collation.hs view
@@ -19,7 +19,6 @@ ) where -import qualified Data.IntSet as IntSet import qualified Data.Text as T import qualified Data.Text.Read as TR import Data.Text (Text)@@ -219,34 +218,37 @@ -- @implicitweights 18D00..18D8F; FB00 # Tangut Supplement -- @implicitweights 1B170..1B2FF; FB01 # Nushu -- @implicitweights 18B00..18CFF; FB02 # Khitan Small Script+-- from PropList.txt in unicode data:+isUnifiedIdeograph :: Int -> Bool+isUnifiedIdeograph cp =+ (cp >= 0x3400 && cp <= 0x4DBF)+ || (cp >= 0x4E00 && cp <= 0x9FFC)+ || (cp >= 0xFA0E && cp <= 0xFA0F)+ || cp == 0xFA11+ || (cp >= 0xFA13 && cp <= 0xFA14)+ || cp == 0xFA1F+ || cp == 0xFA21+ || (cp >= 0xFA23 && cp <= 0xFA24)+ || (cp >= 0xFA27 && cp <= 0xFA29)+ || (cp >= 0x20000 && cp <= 0x2A6DD)+ || (cp >= 0x2A700 && cp <= 0x2B734)+ || (cp >= 0x2B740 && cp <= 0x2B81D)+ || (cp >= 0x2B820 && cp <= 0x2CEA1)+ || (cp >= 0x2CEB0 && cp <= 0x2EBE0)+ || (cp >= 0x30000 && cp <= 0x3134A)++-- from Blocks.txt in unicode data:+isCjkCompatibilityIdeograph :: Int -> Bool+isCjkCompatibilityIdeograph cp = cp >= 0xF900 && cp <= 0xFAFF++isCjkUnifiedIdeograph :: Int -> Bool+isCjkUnifiedIdeograph cp = cp >= 0x4E00 && cp <= 0x9FFF+ calculateImplicitWeight :: Int -> [CollationElement] calculateImplicitWeight cp = [CollationElement False (fromIntegral aaaa) 0x0020 0x0002 0xFFFF, CollationElement False (fromIntegral bbbb) 0 0 0xFFFF] where- range x y = IntSet.fromList [x..y]- singleton = IntSet.singleton- union = IntSet.union- -- from PropList.txt in unicode data:- unifiedIdeographs = range 0x3400 0x4DBF `union`- range 0x4E00 0x9FFC `union`- range 0xFA0E 0xFA0F `union`- singleton 0xFA11 `union`- range 0xFA13 0xFA14 `union`- singleton 0xFA1F `union`- singleton 0xFA21 `union`- range 0xFA23 0xFA24 `union`- range 0xFA27 0xFA29 `union`- range 0x20000 0x2A6DD `union`- range 0x2A700 0x2B734 `union`- range 0x2B740 0x2B81D `union`- range 0x2B820 0x2CEA1 `union`- range 0x2CEB0 0x2EBE0 `union`- range 0x2CEB0 0x2EBE0 `union`- range 0x30000 0x3134A- -- from Blocks.txt in unicode data:- cjkCompatibilityIdeographs = range 0xF900 0xFAFF- cjkUnifiedIdeographs = range 0x4E00 0x9FFF (aaaa, bbbb) = case cp of _ | cp >= 0x17000 , cp <= 0x18AFF -- Tangut and Tangut Components@@ -257,14 +259,14 @@ -> (0xFB01, (cp - 0x1B170) .|. 0x8000) | cp >= 0x18B00 , cp <= 0x18CFF -- Khitan Small Script -> (0xFB02, (cp - 0x18B00) .|. 0x8000)- | cp `IntSet.member` unifiedIdeographs &&- (cp `IntSet.member` cjkUnifiedIdeographs ||- cp `IntSet.member` cjkCompatibilityIdeographs) -- Core Han+ | isUnifiedIdeograph cp &&+ (isCjkUnifiedIdeograph cp ||+ isCjkCompatibilityIdeograph cp) -- Core Han -> (0xFB40 + (cp `shiftR` 15), (cp .&. 0x7FFF) .|. 0x8000)- | cp `IntSet.member` unifiedIdeographs -- All Other Han Unified ?+ | isUnifiedIdeograph cp -- All Other Han Unified ? -> (0xFB80 + (cp `shiftR` 15), (cp .&. 0x7FFF) .|. 0x8000) | otherwise- -> (0xFBC0 + (cp `shiftR` 15), (cp .&. 0x7FFFF) .|. 0x8000)+ -> (0xFBC0 + (cp `shiftR` 15), (cp .&. 0x7FFF) .|. 0x8000) -- | Parse a 'Collation' from a Text in the format of @allkeys.txt@. parseCollation :: Text -> Collation
test/unit.hs view
@@ -21,11 +21,13 @@ main :: IO () main = do conformanceTree <- conformanceTests- defaultMain (tests conformanceTree)+ coverageTree <- implicitWeightCoverageTests+ defaultMain (tests conformanceTree coverageTree) -tests :: TestTree -> TestTree-tests conformanceTree = testGroup "Tests"+tests :: TestTree -> TestTree -> TestTree+tests conformanceTree coverageTree = testGroup "Tests" [ conformanceTree+ , coverageTree , testCase "Sorting test 1" $ sortBy (collate ourCollator) ["hi", "hit", "hít", "hat", "hot", "naïve", "nag", "name"] @?=@@ -172,6 +174,87 @@ es -> assertFailure (unlines es)) $ map (conformanceTestWith coll) (zip3 (map fst xs) (map snd xs) (drop 1 (map snd xs)))++-- | Guard against the hand-coded ideograph ranges in+-- 'Text.Collate.Collation' (used by 'calculateImplicitWeight') drifting+-- out of sync with the shipped Unicode data. Every code point that the+-- data marks as an ideograph must receive an /ideographic/ implicit+-- primary weight; anything not covered by the hand-coded ranges falls to+-- the generic unassigned bucket at @0xFBC0@ and beyond. We enumerate the+-- full set (not just range endpoints) so that adding a new ideograph block+-- to the data without updating the ranges is caught.+--+-- Sources of truth, both in the shipped data:+--+-- * @\@implicitweights@ directives in @data\/allkeys.txt@+-- (Tangut, Nushu, Khitan -> 0xFB00..0xFB02)+-- * @\<... Ideograph ..., First\/Last\>@ markers in+-- @data\/UnicodeData.txt@ (CJK Unified + extensions -> 0xFB40\/0xFB80)+implicitWeightCoverageTests :: IO TestTree+implicitWeightCoverageTests = do+ putStrLn "Loading implicit-weight coverage data..."+ allkeys <- B8.readFile "data/allkeys.txt"+ ucd <- B8.readFile "data/UnicodeData.txt"+ let ranges = implicitWeightDirectives allkeys ++ ideographRanges ucd+ cps = concatMap (\(lo, hi) -> [lo .. hi]) ranges+ return $ testCase "Implicit weights cover all ideographs in shipped data" $+ case [ (cp, w) | cp <- cps, let w = implicitPrimary cp, w >= 0xFBC0 ] of+ [] -> return ()+ bad -> assertFailure $+ "Code points marked as ideographs in the data receive the "+ ++ "generic\n(>= 0xFBC0) implicit weight; the hand-coded ranges "+ ++ "in\nText.Collate.Collation are stale (" ++ show (length bad)+ ++ " affected):\n"+ ++ unlines [ printf " U+%05X -> primary %04X" cp w+ | (cp, w) <- take 40 bad ]++-- | Primary weight assigned to a single code point (head of its sort key).+implicitPrimary :: Int -> Int+implicitPrimary cp =+ case sortKey rootCollator (T.singleton (chr cp)) of+ SortKey (w : _) -> fromIntegral w+ SortKey [] -> 0++-- | Parse the @\@implicitweights LO..HI; WEIGHT@ directives from+-- @allkeys.txt@ into inclusive code-point ranges.+implicitWeightDirectives :: B8.ByteString -> [(Int, Int)]+implicitWeightDirectives = mapMaybe parseLine . B8.lines+ where+ parseLine ln =+ let t = TE.decodeLatin1 ln+ in if "@implicitweights" `T.isPrefixOf` t+ then dotRange (T.takeWhile (/= ';') (T.drop 16 t))+ else Nothing+ dotRange t =+ case T.splitOn ".." (T.strip t) of+ [a, b] -> (,) <$> hexInt a <*> hexInt b+ _ -> Nothing++-- | Parse the @\<... Ideograph ..., First\/Last\>@ range markers from+-- @UnicodeData.txt@ into inclusive code-point ranges.+ideographRanges :: B8.ByteString -> [(Int, Int)]+ideographRanges = pairUp . mapMaybe marker . B8.lines+ where+ marker ln =+ case T.splitOn ";" (TE.decodeLatin1 ln) of+ (cpF : nameF : _)+ | "Ideograph" `T.isInfixOf` nameF+ , Just cp <- hexInt cpF+ -> if ", First>" `T.isSuffixOf` nameF then Just (cp, False)+ else if ", Last>" `T.isSuffixOf` nameF then Just (cp, True)+ else Nothing+ _ -> Nothing+ pairUp ((lo, False) : (hi, True) : rest) = (lo, hi) : pairUp rest+ pairUp (_ : rest) = pairUp rest+ pairUp [] = []++-- | Parse a (possibly space-padded) hexadecimal integer, requiring that it+-- consume the whole input.+hexInt :: Text -> Maybe Int+hexInt t =+ case TR.hexadecimal (T.strip t) of+ Right (n, rest) | T.null (T.strip rest) -> Just n+ _ -> Nothing conformanceTestWith :: Collator -> (Int, Text, Text) -> Either String () conformanceTestWith coll (lineNo, txt1, txt2) =
unicode-collation.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: unicode-collation-version: 0.1.3.6+version: 0.1.3.7 synopsis: Haskell implementation of the Unicode Collation Algorithm description: This library provides a pure Haskell implementation of the Unicode Collation Algorithm described at@@ -35,6 +35,7 @@ GHC == 9.4.2 GHC == 9.6.3 GHC == 9.8.1+ GHC == 9.10.1 source-repository head type: git@@ -49,8 +50,12 @@ Description: Build the unicode-collate executable. Default: False +flag icu-benchmark+ Description: Build the text-icu comparison benchmark (needs ICU C lib).+ Default: False+ common common-options- build-depends: base >= 4.11 && < 4.20+ build-depends: base >= 4.11 && < 4.23 ghc-options: -Wall -Wcompat@@ -149,4 +154,19 @@ , text-icu >= 0.8 , quickcheck-instances , QuickCheck+ ghc-options: -rtsopts -with-rtsopts=-A8m+ if flag(icu-benchmark)+ buildable: True+ else+ buildable: False++benchmark bench-sortkey+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: benchmark+ main-is: SortKey.hs+ build-depends: tasty-bench+ , unicode-collation+ , text+ , deepseq ghc-options: -rtsopts -with-rtsopts=-A8m