packages feed

symbolize (empty) → 0.1.0.0

raw patch · 11 files changed

+825/−0 lines, 11 filesdep +asyncdep +basedep +bytestringsetup-changed

Dependencies added: async, base, bytestring, deepseq, doctest-parallel, hashable, hedgehog, symbolize, tasty, tasty-golden, tasty-hedgehog, tasty-hunit, text, text-display, text-short, unordered-containers

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `symbolize`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## Unreleased++## 0.1.0.0 - YYYY-MM-DD
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2023++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,116 @@+# Symbolize+[![Hackage](http://img.shields.io/hackage/v/symbolize.svg)](https://hackage.haskell.org/package/symbolize)++Haskell library implementing a global Symbol Table, with garbage collection.++Symbols, also known as Atoms or Interned Strings, are a common technique+to reduce memory usage and improve performance when using many small strings.++By storing a single copy of each encountered string in a global table and giving out indexes to that table,+it is possible to compare strings for equality in constant time, instead of linear (in string size) time.++The main advantages of Symbolize over existing symbol table implementations are:++- Garbage collection: Symbols which are no longer used are automatically cleaned up.+- `Symbol`s have a memory footprint of exactly 1 `Word` and are nicely unpacked by GHC.+- Support for any `Textual` type, including `String`, (strict and lazy) `Data.Text`, (strict and lazy) `Data.ByteString` etc.+- Thread-safe.+- Efficient: Calls to `lookup` and `unintern` are free of atomic memory barriers (and never have to wait on a concurrent thread running `intern`)+- Support for a maximum of 2^64 symbols at the same time (you'll probably run out of memory before that point).++## Basic usage++This module is intended to be imported qualified, e.g.+++```haskell+import Symbolize (Symbol)+import qualified Symbolize+```++To intern a string, use `intern`:++```haskell+>>> hello = Symbolize.intern "hello"+>>> world = Symbolize.intern "world"+>>> (hello, world)+(Symbolize.intern "hello",Symbolize.intern "world")+```++Interning supports any `Textual` type, so you can also use `Data.Text` or `Data.ByteString` etc.:++```haskell+>>> import Data.Text (Text)+>>> niceCheeses = fmap Symbolize.intern (["Roquefort", "Camembert", "Brie"] :: [Text])+>>> niceCheeses+[Symbolize.intern "Roquefort",Symbolize.intern "Camembert",Symbolize.intern "Brie"]+```++And if you are using OverloadedStrings, you can use the `IsString` instance to intern constants:++```haskell+>>> hello2 = ("hello" :: Symbol)+>>> hello2+Symbolize.intern "hello"+```++Comparisons between symbols run in O(1) time:++```haskell+>>> hello == hello2+True+>>> hello == world+False+```++To get back the textual value of a symbol, use `unintern`:++```haskell+>>> Symbolize.unintern hello+"hello"+```++If you only want to check whether a string is already interned, use `lookup`:++```haskell+>>> Symbolize.lookup "hello"+Just (Symbolize.intern "hello")+```++Symbols make great keys for `Data.HashMap` and `Data.HashSet`.+Hashing them is a no-op and they are guaranteed to be unique:++```haskell+>>> import qualified Data.Hashable as Hashable+>>> Hashable.hash hello+0+>>> fmap Hashable.hash niceCheeses+[2,3,4]+```++For introspection, you can look at how many symbols currently exist:++```haskell+>>> Symbolize.globalSymbolTableSize+5+>>> [unintern (intern (show x)) | x <- [1..5]]+["1","2","3","4","5"]+>>> Symbolize.globalSymbolTableSize+10+```++Unused symbols will be garbage-collected, so you don't have to worry about memory leaks:++```haskell+>>> System.Mem.performGC+>>> Symbolize.globalSymbolTableSize+5+```++For deeper introspection, you can look at the Show instance of the global symbol table:+/(Note that the exact format is subject to change.)/++```haskell+>>> Symbolize.globalSymbolTable+GlobalSymbolTable { count = 5, next = 10, contents = [(0,"hello"),(1,"world"),(2,"Roquefort"),(3,"Camembert"),(4,"Brie")] }+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Symbolize.hs view
@@ -0,0 +1,379 @@+-- | Implementation of a global Symbol Table, with garbage collection.+--+-- Symbols, also known as Atoms or Interned Strings, are a common technique+-- to reduce memory usage and improve performance when using many small strings.+--+-- By storing a single copy of each encountered string in a global table and giving out indexes to that table,+-- it is possible to compare strings for equality in constant time, instead of linear (in string size) time.+--+-- The main advantages of Symbolize over existing symbol table implementations are:+--+-- - Garbage collection: Symbols which are no longer used are automatically cleaned up.+-- - `Symbol`s have a memory footprint of exactly 1 `Word` and are nicely unpacked by GHC.+-- - Support for any `Textual` type, including `String`, (strict and lazy) `Data.Text`, (strict and lazy) `Data.ByteString` etc.+-- - Thread-safe.+-- - Efficient: Calls to `lookup` and `unintern` are free of atomic memory barriers (and never have to wait on a concurrent thread running `intern`)+-- - Support for a maximum of 2^64 symbols at the same time (you'll probably run out of memory before that point).+--+-- == Basic usage+--+-- This module is intended to be imported qualified, e.g.+--+-- > import Symbolize (Symbol)+-- > import qualified Symbolize+--+-- To intern a string, use `intern`:+--+-- >>> hello = Symbolize.intern "hello"+-- >>> world = Symbolize.intern "world"+-- >>> (hello, world)+-- (Symbolize.intern "hello",Symbolize.intern "world")+--+-- Interning supports any `Textual` type, so you can also use `Data.Text` or `Data.ByteString` etc.:+--+-- >>> import Data.Text (Text)+-- >>> niceCheeses = fmap Symbolize.intern (["Roquefort", "Camembert", "Brie"] :: [Text])+-- >>> niceCheeses+-- [Symbolize.intern "Roquefort",Symbolize.intern "Camembert",Symbolize.intern "Brie"]+--+-- And if you are using OverloadedStrings, you can use the `IsString` instance to intern constants:+--+-- >>> hello2 = ("hello" :: Symbol)+-- >>> hello2+-- Symbolize.intern "hello"+-- >>> Symbolize.intern ("world" :: Text)+-- Symbolize.intern "world"+--+-- Comparisons between symbols run in O(1) time:+--+-- >>> hello == hello2+-- True+-- >>> hello == world+-- False+--+-- To get back the textual value of a symbol, use `unintern`:+--+-- >>> Symbolize.unintern hello+-- "hello"+--+-- If you want to check whether a string is currently interned, use `lookup`:+--+-- >>> Symbolize.lookup "hello"+-- Just (Symbolize.intern "hello")+--+-- Symbols make great keys for `Data.HashMap` and `Data.HashSet`.+-- Hashing them is a no-op and they are guaranteed to be unique:+--+-- >>> Data.Hashable.hash hello+-- 0+-- >>> fmap Data.Hashable.hash niceCheeses+-- [2,3,4]+--+-- For introspection, you can look at how many symbols currently exist:+--+-- >>> Symbolize.globalSymbolTableSize+-- 5+-- >>> [unintern (intern (show x)) | x <- [1..5]]+-- ["1","2","3","4","5"]+-- >>> Symbolize.globalSymbolTableSize+-- 10+--+-- Unused symbols will be garbage-collected, so you don't have to worry about memory leaks:+--+-- >>> System.Mem.performGC+-- >>> Symbolize.globalSymbolTableSize+-- 5+--+-- For deeper introspection, you can look at the Show instance of the global symbol table:+-- /(Note that the exact format is subject to change.)/+--+-- >>> Symbolize.globalSymbolTable+-- GlobalSymbolTable { count = 5, next = 10, contents = [(0,"hello"),(1,"world"),(2,"Roquefort"),(3,"Camembert"),(4,"Brie")] }+module Symbolize+  ( -- * Symbol+    Symbol,+    intern,+    unintern,+    lookup,+    Textual (..),++    -- * Introspection & Metrics+    GlobalSymbolTable,+    globalSymbolTable,+    globalSymbolTableSize,+  )+where++import Control.Applicative ((<|>))+import Control.Concurrent.MVar (MVar)+import qualified Control.Concurrent.MVar as MVar+import Control.DeepSeq (NFData (..))+import Data.Function ((&))+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.Hashable (Hashable (..))+import Data.IORef (IORef)+import qualified Data.IORef as IORef+import Data.String (IsString (..))+import Data.Text.Display (Display (..))+import Data.Text.Short (ShortText)+import GHC.Read (Read (..))+import qualified Symbolize.Accursed+import Symbolize.Textual (Textual (..))+import qualified System.IO.Unsafe+import System.Mem.Weak (Weak)+import qualified System.Mem.Weak as Weak+import Text.Read (Lexeme (Ident), lexP, parens, prec, readListPrecDefault)+import qualified Text.Read+import Prelude hiding (lookup)++-- | A string-like type with O(1) equality and comparison.+--+-- A Symbol represents a string (any `Textual`, so String, Text, ByteString etc.)+-- However, it only stores an (unpacked) `Word`, used as index into a global table in which the actual string value is stored.+-- Thus equality checks are constant-time, and its memory footprint is very low.+--+-- This is very useful if you're frequently comparing strings+-- and the same strings might come up many times.+-- It also makes Symbol a great candidate for a key in a `HashMap` or `Data.HashSet`. (Hashing them is a no-op!)+--+-- The symbol table is implemented using weak pointers,+-- which means that unused symbols will be garbage collected.+-- As such, you do not need to be concerned about memory leaks.+--+-- Symbols are considered 'the same' regardless of whether they originate+-- from a `String`, (lazy or strict, normal or short) `Data.Text`, (lazy or strict, normal or short) `Data.ByteString` etc.+--+-- Symbolize supports up to 2^64 symbols existing at the same type.+-- Your system will probably run out of memory before you reach that point.+data Symbol = Symbol {-# UNPACK #-} !Word++instance Show Symbol where+  showsPrec p symbol =+    let !str = unintern @String symbol+     in showParen (p > 10) $+          showString "Symbolize.intern " . shows str++-- | To be a good citizen w.r.t both `Show` and `IsString`, reading is supported two ways:+--+-- >>> read @Symbol "Symbolize.intern \"Haskell\""+-- Symbolize.intern "Haskell"+-- >>> read @Symbol "\"Curry\""+-- Symbolize.intern "Curry"+instance Read Symbol where+  readListPrec = readListPrecDefault+  readPrec = parens $ prec 10 $ full <|> onlyString+    where+      onlyString = do+        str <- readPrec @String+        return $ Symbolize.intern str+      full = do+        Ident "Symbolize" <- lexP+        Text.Read.Symbol "." <- lexP+        Ident "intern" <- lexP+        str <- readPrec @String+        return $ Symbolize.intern str++instance IsString Symbol where+  fromString = intern+  {-# INLINE fromString #-}++-- |+-- >>> Data.Text.Display.display (Symbolize.intern "Pizza")+-- "Pizza"+instance Display Symbol where+  displayBuilder = unintern+  {-# INLINE displayBuilder #-}++-- | Takes only O(1) time.+instance Eq Symbol where+  (Symbol a) == (Symbol b) = a == b+  {-# INLINE (==) #-}++-- | Symbol contains only a strict `Word`, so it is already fully evaluated.+instance NFData Symbol where+  rnf sym = seq sym ()++-- | Symbols are ordered by their `ShortText` representation.+--+-- Comparison takes O(n) time, as they are compared byte-by-byte.+instance Ord Symbol where+  compare a b = compare (unintern @ShortText a) (unintern @ShortText b)+  {-# INLINE compare #-}++-- |+-- Hashing a `Symbol` is very fast:+--+-- `hash` is a no-op and results in zero collissions, as `Symbol`'s index is unique and can be interpreted as a hash as-is.+--+-- `hashWithSalt` takes O(1) time; just as long as hashWithSalt-ing any other `Word`.+instance Hashable Symbol where+  hash (Symbol idx) = hash idx+  hashWithSalt salt (Symbol idx) = hashWithSalt salt idx+  {-# INLINE hash #-}+  {-# INLINE hashWithSalt #-}++-- | The global Symbol Table, containing a bidirectional mapping between each symbol's textual representation and its Word index.+--+-- You cannot manipulate the table itself directly,+-- but you can use `globalSymbolTable` to get a handle to it and use its `Show` instance for introspection.+--+-- `globalSymbolTableSize` can similarly be used to get the current size of the table.+data GlobalSymbolTable = GlobalSymbolTable+  { next :: !(MVar Word),+    mappings :: !(IORef SymbolTableMappings)+  }++instance Show GlobalSymbolTable where+  show table =+    -- SAFETY: We're only reading, and do not care about performance here.+    System.IO.Unsafe.unsafePerformIO $ do+      -- NOTE: We want to make sure that (roughly) the same table state is used for each of the components+      -- which is why we use BangPatterns such that a partially-read show string will end up printing a (roughly) consistent state.+      !next' <- MVar.readMVar (next table) -- IORef.readIORef (next table)+      !mappings' <- IORef.readIORef (mappings table)+      let !contents = mappings' & symbolsToText+      -- let !reverseContents = mappings' & textToSymbols & fmap  (fmap hash . System.IO.Unsafe.unsafePerformIO . Weak.deRefWeak) & HashMap.toList+      let !count = HashMap.size contents+      pure+        $ "GlobalSymbolTable { count = "+          <> show count+          <> ", next = "+          <> show next'+          -- <> ", reverseContents = "+          -- <> show reverseContents+          <> ", contents = "+          <> show (HashMap.toList contents)+          <> " }"++data SymbolTableMappings = SymbolTableMappings+  { textToSymbols :: !(HashMap ShortText (Weak Symbol)),+    symbolsToText :: !(HashMap Word ShortText)+  }++-- | Unintern a symbol, returning its textual value.+-- Takes O(log16 n) time to look up the matching textual value, where n is the number of symbols currently in the table.+--+-- Afterwards, the textual value is converted to the desired type s. See `Textual` for the type-specific time complexity.+--+-- Runs concurrently with any other operation on the symbol table, without any atomic memory barriers.+unintern :: (Textual s) => Symbol -> s+unintern (Symbol idx) =+  let !mappingsRef = mappings globalSymbolTable'+      -- SAFETY:+      -- First, it's thread-safe because we only read (from a single IORef).+      -- Second, this function is idempotent and (outwardly) pure,+      -- so whether it is executed only once or many times for a particular Symbol does not matter in the slightest.+      -- Thus, we're very happy with the compiler inlining, CSE'ing or floating out this IO action.+      --+      -- I hope I'm correct and the Cosmic Horror will not eat me!+      -- signed by Marten, 2023-11-24+      !mappings' = Symbolize.Accursed.accursedUnutterablePerformIO $ IORef.readIORef mappingsRef+   in mappings'+        & symbolsToText+        & HashMap.lookup idx+        & maybe (error ("Symbol " <> show idx <> " not found. This should never happen" <> show globalSymbolTable')) fromShortText+{-# INLINE unintern #-}++-- | Looks up a symbol in the global symbol table.+--+-- Returns `Nothing` if no such symbol currently exists.+--+-- Takes O(log16 n) time, where n is the number of symbols currently in the table.+--+-- Runs concurrently with any other operation on the symbol table, without any atomic memory barriers.+--+-- Because the result can vary depending on the current state of the symbol table, this function is not pure.+lookup :: (Textual s) => s -> IO (Maybe Symbol)+lookup text = do+  let !text' = toShortText text+  table <- globalSymbolTable+  mappings <- IORef.readIORef (mappings table)+  let maybeWeak = mappings & textToSymbols & HashMap.lookup text'+  case maybeWeak of+    Nothing -> pure Nothing+    Just weak -> do+      Weak.deRefWeak weak+{-# NOINLINE lookup #-}++-- | Intern a string-like value.+--+-- First converts s to a `ShortText` (if it isn't already one). See `Textual` for the type-specific time complexity of this.+-- Then, takes O(log16 n) time to look up the matching symbol and insert it if it did not exist yet (where n is the number of symbols currently in the table).+--+-- Any concurrent calls to (the critical section in) `intern` are synchronized.+intern :: (Textual s) => s -> Symbol+intern text =+  let !text' = toShortText text+   in lookupOrInsert text'+  where+    lookupOrInsert text' =+      -- SAFETY: `intern` is idempotent, so inlining and CSE is benign (and might indeed improve performance).+      System.IO.Unsafe.unsafePerformIO $ MVar.modifyMVar (next globalSymbolTable') $ \next -> do+        maybeWeak <- lookup text+        case maybeWeak of+          Just symbol -> pure (next, symbol)+          Nothing -> insert text' next+    insert text' next = do+      SymbolTableMappings {symbolsToText, textToSymbols} <- IORef.readIORef (mappings globalSymbolTable')+      let !idx = nextEmptyIndex next symbolsToText+      let !symbol = Symbol idx+      weakSymbol <- Weak.mkWeakPtr symbol (Just (finalizer idx))+      let !mappings2 =+            SymbolTableMappings+              { symbolsToText = HashMap.insert idx text' symbolsToText,+                textToSymbols = HashMap.insert text' weakSymbol textToSymbols+              }+      IORef.atomicWriteIORef (mappings globalSymbolTable') mappings2++      let !nextFree = idx + 1+      pure (nextFree, symbol)+{-# INLINE intern #-}++nextEmptyIndex :: Word -> HashMap Word ShortText -> Word+nextEmptyIndex starting symbolsToText = go starting+  where+    go idx = case HashMap.lookup idx symbolsToText of+      Nothing -> idx+      _ -> go (idx + 1) -- <- Wrapping on overflow is intentional and important for correctness++-- | Returns a handle to the global symbol table. (Only) useful for introspection or debugging.+globalSymbolTable :: IO GlobalSymbolTable+globalSymbolTable =+  globalSymbolTable'+    -- re-introduce the IO bound which we unsafePerformIO'ed:+    & pure++globalSymbolTable' :: GlobalSymbolTable+globalSymbolTable' =+  -- SAFETY: We need all calls to globalSymbolTable' to use the same thunk, so NOINLINE.+  System.IO.Unsafe.unsafePerformIO $ do+    nextRef <- MVar.newMVar 0 -- IORef.newIORef 0+    mappingsRef <- IORef.newIORef (SymbolTableMappings HashMap.empty HashMap.empty)+    return (GlobalSymbolTable nextRef mappingsRef)+{-# NOINLINE globalSymbolTable' #-}++-- | Returns the current size of the global symbol table. Useful for introspection or metrics.+globalSymbolTableSize :: IO Word+globalSymbolTableSize = do+  table <- globalSymbolTable+  mappings <- IORef.readIORef (mappings table)+  let size =+        mappings+          & symbolsToText+          & HashMap.size+          & fromIntegral+  pure size++finalizer :: Word -> IO ()+finalizer idx = do+  MVar.withMVar (next globalSymbolTable') $ \_next -> do+    IORef.modifyIORef' (mappings globalSymbolTable') $ \SymbolTableMappings {symbolsToText, textToSymbols} ->+      case HashMap.lookup idx symbolsToText of+        Nothing -> error ("Duplicate finalizer called for " <> show idx <> "This should never happen") -- SymbolTableMappings {symbolsToText, textToSymbols}+        Just text ->+          SymbolTableMappings+            { symbolsToText = HashMap.delete idx symbolsToText,+              textToSymbols = HashMap.delete text textToSymbols+            }+{-# NOINLINE finalizer #-}
+ src/Symbolize/Accursed.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE MagicHash, UnboxedTuples #-}+module Symbolize.Accursed (accursedUnutterablePerformIO) where++import GHC.IO (IO(IO))+import GHC.Base (realWorld#)++-- This \"function\" has a superficial similarity to 'System.IO.Unsafe.unsafePerformIO' but+-- it is in fact a malevolent agent of chaos.+--+-- Full warning: https://hackage.haskell.org/package/bytestring-0.12.0.2/docs/Data-ByteString-Internal.html#v:accursedUnutterablePerformIO+-- (This definition is also taken from there)+accursedUnutterablePerformIO :: IO a -> a+accursedUnutterablePerformIO (IO m) = case m realWorld# of (# _, r #)   -> r+{-# INLINE accursedUnutterablePerformIO #-}
+ src/Symbolize/Textual.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE FlexibleInstances #-}+-- NOTE: FlexibleInstances is needed to support `String` instance :-(++module Symbolize.Textual (Textual (..)) where++import Data.ByteString (ByteString)+import Data.ByteString.Short (ShortByteString)+import qualified Data.ByteString.Short as ShortByteString+import Data.Function ((&))+import Data.Text (Text)+import qualified Data.Text.Encoding as Text.Encoding+import qualified Data.Text.Encoding.Error as Text.Encoding.Error+import qualified Data.Text.Lazy as LText+import Data.Text.Short (ShortText)+import qualified Data.Text.Short as ShortText+import Data.Text.Lazy.Builder (Builder)+import qualified Data.Text.Lazy.Builder as Builder++-- | Implemented by any String-like types.+-- The symbol table uses `ShortText` for its internal storage, so any type which can be converted to it+-- can be turned to/from a `Symbolize.Symbol`.+--+-- Instance should handle potential invalid UTF-8 by using the Unicode replacement character,+-- c.f. `Data.Text.Encoding.Error.lenientDecode`.+class Textual a where+  toShortText :: a -> ShortText+  fromShortText :: ShortText -> a++-- |+-- - O(0) conversion (a no-op)+instance Textual ShortText where+  toShortText = id+  {-# INLINE toShortText #-}+  fromShortText = id+  {-# INLINE fromShortText #-}++-- |+-- - O(1) conversion+instance Textual Text where+  toShortText = ShortText.fromText+  {-# INLINE toShortText #-}+  fromShortText = ShortText.toText+  {-# INLINE fromShortText #-}++-- |+-- - O(n) conversion+instance Textual String where+  toShortText = ShortText.fromString+  {-# INLINE toShortText #-}+  fromShortText = ShortText.toString+  {-# INLINE fromShortText #-}++-- |+-- - O(1) conversion+instance Textual LText.Text where+  toShortText = ShortText.fromText . LText.toStrict+  {-# INLINE toShortText #-}+  fromShortText = LText.fromStrict . ShortText.toText+  {-# INLINE fromShortText #-}++-- | +-- - toShortText: O(n). Evaluates the entire builder.+-- - fromShortText: O(1)+instance Textual Builder where+  toShortText = ShortText.fromText . LText.toStrict . Builder.toLazyText+  {-# INLINE toShortText #-}+  fromShortText = Builder.fromText . ShortText.toText+  {-# INLINE fromShortText #-}++-- |+-- - toShortText: O(n). Turns invalid UTF-8 into the Unicode replacement character.+-- - fromShortText: O(0) no-op+instance Textual ShortByteString where+  toShortText byteString =+    byteString+      & ShortByteString.fromShort+      & Text.Encoding.decodeUtf8With Text.Encoding.Error.lenientDecode+      & ShortText.fromText+  {-# INLINE toShortText #-}++  fromShortText = ShortText.toShortByteString+  {-# INLINE fromShortText #-}++-- |+-- - toShortText: O(n). Turns invalid UTF-8 into the Unicode replacement character.+-- - fromShortText: O(n).+instance Textual ByteString where+  toShortText byteString =+    byteString+      & Text.Encoding.decodeUtf8With Text.Encoding.Error.lenientDecode+      & ShortText.fromText+  {-# INLINE toShortText #-}++  fromShortText = ShortText.toByteString+  {-# INLINE fromShortText #-}
+ symbolize.cabal view
@@ -0,0 +1,122 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.36.0.+--+-- see: https://github.com/sol/hpack++name:           symbolize+version:        0.1.0.0+synopsis:       Efficient global Symbol Table, with Garbage Collection.+description:     Symbols, also known as Atoms or Interned Strings, are a common technique to reduce memory usage and improve performance when using many small strings.+                By storing a single copy of each encountered string in a global table and giving out indexes to that table, it is possible to compare strings for equality in constant time, instead of linear (in string size) time.+                The main advantages of Symbolize over existing symbol table implementations are:+                - Garbage collection: Symbols which are no longer used are automatically cleaned up. - `Symbol`s have a memory footprint of exactly 1 `Word` and are nicely unpacked by GHC. - Support for any `Textual` type, including `String`, (strict and lazy) `Data.Text`, (strict and lazy) `Data.ByteString` etc. - Thread-safe. - Calls to `lookup` and `unintern` are free of atomic memory barriers (and never have to wait on a concurrent thread running `intern`) - Support for a maximum of 2^64 symbols at the same time (you'll probably run out of memory before that point).+                Please see the full README on GitHub at <https://github.com/Qqwy/haskell-symbolize#readme> +category:       Data, Data Structures+homepage:       https://github.com/Qqwy/haskell-symbolize#readme+bug-reports:    https://github.com/Qqwy/haskell-symbolize/issues+author:         Qqwy / Marten+maintainer:     qqwy@gmx.com+copyright:      2023 Marten Wijnja+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/Qqwy/haskell-symbolize++library+  exposed-modules:+      Symbolize+  other-modules:+      Symbolize.Textual+      Symbolize.Accursed+  hs-source-dirs:+      src+  default-extensions:+      BangPatterns+      OverloadedStrings+      DeriveAnyClass+      TypeApplications+      NamedFieldPuns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base >=4.7 && <5+    , bytestring >=0.11.0 && <0.12+    , deepseq >=1.4.0 && <1.5+    , hashable >=1.4.0 && <1.5+    , text >=2.0 && <2.2+    , text-display >=0.0.5 && <0.1+    , text-short >=0.1.0 && <0.2+    , unordered-containers >=0.2.0 && <0.3+  default-language: Haskell2010++test-suite symbolize-doctest+  type: exitcode-stdio-1.0+  main-is: DocTest.hs+  other-modules:+      Paths_symbolize+  autogen-modules:+      Paths_symbolize+  hs-source-dirs:+      test/doctest+  default-extensions:+      BangPatterns+      OverloadedStrings+      DeriveAnyClass+      TypeApplications+      NamedFieldPuns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , bytestring >=0.11.0 && <0.12+    , deepseq >=1.4.0 && <1.5+    , doctest-parallel+    , hashable >=1.4.0 && <1.5+    , symbolize+    , text >=2.0 && <2.2+    , text-display >=0.0.5 && <0.1+    , text-short >=0.1.0 && <0.2+    , unordered-containers >=0.2.0 && <0.3+  default-language: Haskell2010++test-suite symbolize-test+  type: exitcode-stdio-1.0+  main-is: Suite.hs+  other-modules:+      SymbolizeTest+      Paths_symbolize+  autogen-modules:+      Paths_symbolize+  hs-source-dirs:+      test/suite+  default-extensions:+      BangPatterns+      OverloadedStrings+      DeriveAnyClass+      TypeApplications+      NamedFieldPuns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-tool-depends:+      tasty-discover:tasty-discover+  build-depends:+      async+    , base >=4.7 && <5+    , bytestring >=0.11.0 && <0.12+    , deepseq >=1.4.0 && <1.5+    , hashable >=1.4.0 && <1.5+    , hedgehog+    , symbolize+    , tasty+    , tasty-golden+    , tasty-hedgehog+    , tasty-hunit+    , text >=2.0 && <2.2+    , text-display >=0.0.5 && <0.1+    , text-short >=0.1.0 && <0.2+    , unordered-containers >=0.2.0 && <0.3+  default-language: Haskell2010
+ test/doctest/DocTest.hs view
@@ -0,0 +1,5 @@+import Test.DocTest (mainFromCabal)+import System.Environment (getArgs)++main :: IO ()+main = mainFromCabal "symbolize" =<< getArgs
+ test/suite/Suite.hs view
@@ -0,0 +1,5 @@+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}+-- | Tasty test driver module. Auto-discovers any tests+-- in this directory and subdirectories and runs them.+--+-- c.f. https://hackage.haskell.org/package/tasty-discover
+ test/suite/SymbolizeTest.hs view
@@ -0,0 +1,46 @@+module SymbolizeTest where++-- import qualified System.Mem++import qualified Control.Concurrent.Async+import Control.Monad.IO.Class (liftIO)+import qualified Data.Hashable+import Data.Text (Text)+import Hedgehog+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import qualified Symbolize+import qualified System.Mem+import Test.Tasty.HUnit++unit_simpleInternUninternTest :: IO ()+unit_simpleInternUninternTest = do+  System.Mem.performGC++  let str = "hello" :: Text+  let !symbol = Symbolize.intern str++  size <- Symbolize.globalSymbolTableSize+  size @?= 1++  let str2 = Symbolize.unintern symbol+  str2 @?= str++hprop_symbolTableIsIdempotent :: Property+hprop_symbolTableIsIdempotent = withTests 1000 $ property $ do+  texts <- forAll $ Gen.list (Range.linear 0 200) (Gen.text (Range.linear 0 20) Gen.unicode)+  let !symbols = fmap Symbolize.intern texts+  annotateShow (fmap Data.Hashable.hash symbols)+  let !texts2 = fmap Symbolize.unintern symbols++  texts2 === texts++hprop_concurrentAccessDoesNotCorruptTable :: Property+hprop_concurrentAccessDoesNotCorruptTable = withTests 500 $ property $ do+  let numCores = 8+  texts <- forAll $ Gen.list (Range.linear 0 200) (Gen.text (Range.linear 0 20) Gen.unicode)+  results <- liftIO $ Control.Concurrent.Async.forConcurrently [(1 :: Integer) .. numCores] $ \_ -> do+    let !texts2 = fmap (\val -> Symbolize.unintern $! Symbolize.intern $! val) texts+    pure texts2++  mapM_ (=== texts) results