packages feed

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 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