-- | 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.Map.Strict (Map)
import qualified Data.Map.Strict as Map
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 :: Int -> Symbol -> ShowS
showsPrec Int
p Symbol
symbol =
    let !str :: [Char]
str = forall s. Textual s => Symbol -> s
unintern @String Symbol
symbol
     in Bool -> ShowS -> ShowS
showParen (Int
p forall a. Ord a => a -> a -> Bool
> Int
10) forall a b. (a -> b) -> a -> b
$
          [Char] -> ShowS
showString [Char]
"Symbolize.intern " forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. Show a => a -> ShowS
shows [Char]
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 :: ReadPrec [Symbol]
readListPrec = forall a. Read a => ReadPrec [a]
readListPrecDefault
  readPrec :: ReadPrec Symbol
readPrec = forall a. ReadPrec a -> ReadPrec a
parens forall a b. (a -> b) -> a -> b
$ forall a. Int -> ReadPrec a -> ReadPrec a
prec Int
10 forall a b. (a -> b) -> a -> b
$ ReadPrec Symbol
full forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> ReadPrec Symbol
onlyString
    where
      onlyString :: ReadPrec Symbol
onlyString = do
        [Char]
str <- forall a. Read a => ReadPrec a
readPrec @String
        forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall s. Textual s => s -> Symbol
Symbolize.intern [Char]
str
      full :: ReadPrec Symbol
full = do
        Ident [Char]
"Symbolize" <- ReadPrec Lexeme
lexP
        Text.Read.Symbol [Char]
"." <- ReadPrec Lexeme
lexP
        Ident [Char]
"intern" <- ReadPrec Lexeme
lexP
        [Char]
str <- forall a. Read a => ReadPrec a
readPrec @String
        forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall s. Textual s => s -> Symbol
Symbolize.intern [Char]
str

instance IsString Symbol where
  fromString :: [Char] -> Symbol
fromString = forall s. Textual s => s -> Symbol
intern
  {-# INLINE fromString #-}

-- |
-- >>> Data.Text.Display.display (Symbolize.intern "Pizza")
-- "Pizza"
instance Display Symbol where
  displayBuilder :: Symbol -> Builder
displayBuilder = forall s. Textual s => Symbol -> s
unintern
  {-# INLINE displayBuilder #-}

-- | Takes only O(1) time.
instance Eq Symbol where
  (Symbol Word
a) == :: Symbol -> Symbol -> Bool
== (Symbol Word
b) = Word
a forall a. Eq a => a -> a -> Bool
== Word
b
  {-# INLINE (==) #-}

-- | Symbol contains only a strict `Word`, so it is already fully evaluated.
instance NFData Symbol where
  rnf :: Symbol -> ()
rnf Symbol
sym = seq :: forall a b. a -> b -> b
seq Symbol
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 :: Symbol -> Symbol -> Ordering
compare Symbol
a Symbol
b = forall a. Ord a => a -> a -> Ordering
compare (forall s. Textual s => Symbol -> s
unintern @ShortText Symbol
a) (forall s. Textual s => Symbol -> s
unintern @ShortText Symbol
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 -> Int
hash (Symbol Word
idx) = forall a. Hashable a => a -> Int
hash Word
idx
  hashWithSalt :: Int -> Symbol -> Int
hashWithSalt Int
salt (Symbol Word
idx) = forall a. Hashable a => Int -> a -> Int
hashWithSalt Int
salt Word
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.
--
-- Current implementation details (these might change even between PVP-compatible versions):
-- 
-- - A (containers) `Map` is used for mapping text -> symbol. This has O(log2(n)) lookup time, but is resistent to HashDoS attacks.
-- - A (unordered-containers) `HashMap` is used for mapping symbol -> text. This has O(log16(n)) lookup time. 
--   Because symbols are unique and their values are not user-generated, there is no danger of HashDoS here.
data GlobalSymbolTable = GlobalSymbolTable
  { GlobalSymbolTable -> MVar Word
next :: !(MVar Word),
    GlobalSymbolTable -> IORef SymbolTableMappings
mappings :: !(IORef SymbolTableMappings)
  }

instance Show GlobalSymbolTable where
  show :: GlobalSymbolTable -> [Char]
show GlobalSymbolTable
table =
    -- SAFETY: We're only reading, and do not care about performance here.
    forall a. IO a -> a
System.IO.Unsafe.unsafePerformIO forall a b. (a -> b) -> a -> b
$ 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.
      !Word
next' <- forall a. MVar a -> IO a
MVar.readMVar (GlobalSymbolTable -> MVar Word
next GlobalSymbolTable
table) -- IORef.readIORef (next table)
      !SymbolTableMappings
mappings' <- forall a. IORef a -> IO a
IORef.readIORef (GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
table)
      let !contents :: HashMap Word ShortText
contents = SymbolTableMappings
mappings' forall a b. a -> (a -> b) -> b
& SymbolTableMappings -> HashMap Word ShortText
symbolsToText
      -- let !reverseContents = mappings' & textToSymbols & fmap  (fmap hash . System.IO.Unsafe.unsafePerformIO . Weak.deRefWeak) & HashMap.toList
      let !count :: Int
count = forall k v. HashMap k v -> Int
HashMap.size HashMap Word ShortText
contents
      forall (f :: * -> *) a. Applicative f => a -> f a
pure
        forall a b. (a -> b) -> a -> b
$ [Char]
"GlobalSymbolTable { count = "
          forall a. Semigroup a => a -> a -> a
<> forall a. Show a => a -> [Char]
show Int
count
          forall a. Semigroup a => a -> a -> a
<> [Char]
", next = "
          forall a. Semigroup a => a -> a -> a
<> forall a. Show a => a -> [Char]
show Word
next'
          -- <> ", reverseContents = "
          -- <> show reverseContents
          forall a. Semigroup a => a -> a -> a
<> [Char]
", contents = "
          forall a. Semigroup a => a -> a -> a
<> forall a. Show a => a -> [Char]
show (forall k v. HashMap k v -> [(k, v)]
HashMap.toList HashMap Word ShortText
contents)
          forall a. Semigroup a => a -> a -> a
<> [Char]
" }"

data SymbolTableMappings = SymbolTableMappings
  { SymbolTableMappings -> Map ShortText (Weak Symbol)
textToSymbols :: !(Map ShortText (Weak Symbol)),
    SymbolTableMappings -> HashMap Word ShortText
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 :: forall s. Textual s => Symbol -> s
unintern (Symbol Word
idx) =
  let !mappingsRef :: IORef SymbolTableMappings
mappingsRef = GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
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' :: SymbolTableMappings
mappings' = forall a. IO a -> a
Symbolize.Accursed.accursedUnutterablePerformIO forall a b. (a -> b) -> a -> b
$ forall a. IORef a -> IO a
IORef.readIORef IORef SymbolTableMappings
mappingsRef
   in SymbolTableMappings
mappings'
        forall a b. a -> (a -> b) -> b
& SymbolTableMappings -> HashMap Word ShortText
symbolsToText
        forall a b. a -> (a -> b) -> b
& forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HashMap.lookup Word
idx
        forall a b. a -> (a -> b) -> b
& forall b a. b -> (a -> b) -> Maybe a -> b
maybe (forall a. HasCallStack => [Char] -> a
error ([Char]
"Symbol " forall a. Semigroup a => a -> a -> a
<> forall a. Show a => a -> [Char]
show Word
idx forall a. Semigroup a => a -> a -> a
<> [Char]
" not found. This should never happen" forall a. Semigroup a => a -> a -> a
<> forall a. Show a => a -> [Char]
show GlobalSymbolTable
globalSymbolTable')) forall a. Textual a => ShortText -> a
fromShortText
{-# INLINE unintern #-}

-- | Looks up a symbol in the global symbol table.
--
-- Returns `Nothing` if no such symbol currently exists.
--
-- Takes O(log2(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 :: forall s. Textual s => s -> IO (Maybe Symbol)
lookup s
text = do
  let !text' :: ShortText
text' = forall a. Textual a => a -> ShortText
toShortText s
text
  GlobalSymbolTable
table <- IO GlobalSymbolTable
globalSymbolTable
  SymbolTableMappings
mappings <- forall a. IORef a -> IO a
IORef.readIORef (GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
table)
  let maybeWeak :: Maybe (Weak Symbol)
maybeWeak = SymbolTableMappings
mappings forall a b. a -> (a -> b) -> b
& SymbolTableMappings -> Map ShortText (Weak Symbol)
textToSymbols forall a b. a -> (a -> b) -> b
& forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup ShortText
text'
  case Maybe (Weak Symbol)
maybeWeak of
    Maybe (Weak Symbol)
Nothing -> forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a. Maybe a
Nothing
    Just Weak Symbol
weak -> do
      forall v. Weak v -> IO (Maybe v)
Weak.deRefWeak Weak Symbol
weak

-- | 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(log2(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 :: forall s. Textual s => s -> Symbol
intern s
text =
  let !text' :: ShortText
text' = forall a. Textual a => a -> ShortText
toShortText s
text
   in ShortText -> Symbol
lookupOrInsert ShortText
text'
  where
    lookupOrInsert :: ShortText -> Symbol
lookupOrInsert ShortText
text' =
      -- SAFETY: `intern` is idempotent, so inlining and CSE is benign (and might indeed improve performance).
      forall a. IO a -> a
System.IO.Unsafe.unsafePerformIO forall a b. (a -> b) -> a -> b
$ forall a b. MVar a -> (a -> IO (a, b)) -> IO b
MVar.modifyMVar (GlobalSymbolTable -> MVar Word
next GlobalSymbolTable
globalSymbolTable') forall a b. (a -> b) -> a -> b
$ \Word
next -> do
        Maybe Symbol
maybeWeak <- forall s. Textual s => s -> IO (Maybe Symbol)
lookup s
text
        case Maybe Symbol
maybeWeak of
          Just Symbol
symbol -> forall (f :: * -> *) a. Applicative f => a -> f a
pure (Word
next, Symbol
symbol)
          Maybe Symbol
Nothing -> ShortText -> Word -> IO (Word, Symbol)
insert ShortText
text' Word
next
    insert :: ShortText -> Word -> IO (Word, Symbol)
insert ShortText
text' Word
next = do
      SymbolTableMappings {HashMap Word ShortText
symbolsToText :: HashMap Word ShortText
symbolsToText :: SymbolTableMappings -> HashMap Word ShortText
symbolsToText, Map ShortText (Weak Symbol)
textToSymbols :: Map ShortText (Weak Symbol)
textToSymbols :: SymbolTableMappings -> Map ShortText (Weak Symbol)
textToSymbols} <- forall a. IORef a -> IO a
IORef.readIORef (GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
globalSymbolTable')
      let !idx :: Word
idx = Word -> HashMap Word ShortText -> Word
nextEmptyIndex Word
next HashMap Word ShortText
symbolsToText
      let !symbol :: Symbol
symbol = Word -> Symbol
Symbol Word
idx
      Weak Symbol
weakSymbol <- forall k. k -> Maybe (IO ()) -> IO (Weak k)
Weak.mkWeakPtr Symbol
symbol (forall a. a -> Maybe a
Just (Word -> IO ()
finalizer Word
idx))
      let !mappings2 :: SymbolTableMappings
mappings2 =
            SymbolTableMappings
              { symbolsToText :: HashMap Word ShortText
symbolsToText = forall k v.
(Eq k, Hashable k) =>
k -> v -> HashMap k v -> HashMap k v
HashMap.insert Word
idx ShortText
text' HashMap Word ShortText
symbolsToText,
                textToSymbols :: Map ShortText (Weak Symbol)
textToSymbols = forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert ShortText
text' Weak Symbol
weakSymbol Map ShortText (Weak Symbol)
textToSymbols
              }
      forall a. IORef a -> a -> IO ()
IORef.atomicWriteIORef (GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
globalSymbolTable') SymbolTableMappings
mappings2

      let !nextFree :: Word
nextFree = Word
idx forall a. Num a => a -> a -> a
+ Word
1
      forall (f :: * -> *) a. Applicative f => a -> f a
pure (Word
nextFree, Symbol
symbol)
{-# INLINE intern #-}

nextEmptyIndex :: Word -> HashMap Word ShortText -> Word
nextEmptyIndex :: Word -> HashMap Word ShortText -> Word
nextEmptyIndex Word
starting HashMap Word ShortText
symbolsToText = Word -> Word
go Word
starting
  where
    go :: Word -> Word
go Word
idx = case forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HashMap.lookup Word
idx HashMap Word ShortText
symbolsToText of
      Maybe ShortText
Nothing -> Word
idx
      Maybe ShortText
_ -> Word -> Word
go (Word
idx forall a. Num a => a -> a -> a
+ Word
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 :: IO GlobalSymbolTable
globalSymbolTable =
  GlobalSymbolTable
globalSymbolTable'
    -- re-introduce the IO bound which we unsafePerformIO'ed:
    forall a b. a -> (a -> b) -> b
& forall (f :: * -> *) a. Applicative f => a -> f a
pure

globalSymbolTable' :: GlobalSymbolTable
globalSymbolTable' :: GlobalSymbolTable
globalSymbolTable' =
  -- SAFETY: We need all calls to globalSymbolTable' to use the same thunk, so NOINLINE.
  forall a. IO a -> a
System.IO.Unsafe.unsafePerformIO forall a b. (a -> b) -> a -> b
$ do
    MVar Word
nextRef <- forall a. a -> IO (MVar a)
MVar.newMVar Word
0 -- IORef.newIORef 0
    IORef SymbolTableMappings
mappingsRef <- forall a. a -> IO (IORef a)
IORef.newIORef (Map ShortText (Weak Symbol)
-> HashMap Word ShortText -> SymbolTableMappings
SymbolTableMappings forall k a. Map k a
Map.empty forall k v. HashMap k v
HashMap.empty)
    forall (m :: * -> *) a. Monad m => a -> m a
return (MVar Word -> IORef SymbolTableMappings -> GlobalSymbolTable
GlobalSymbolTable MVar Word
nextRef IORef SymbolTableMappings
mappingsRef)
{-# NOINLINE globalSymbolTable' #-}

-- | Returns the current size of the global symbol table. Useful for introspection or metrics.
globalSymbolTableSize :: IO Word
globalSymbolTableSize :: IO Word
globalSymbolTableSize = do
  GlobalSymbolTable
table <- IO GlobalSymbolTable
globalSymbolTable
  SymbolTableMappings
mappings <- forall a. IORef a -> IO a
IORef.readIORef (GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
table)
  let size :: Word
size =
        SymbolTableMappings
mappings
          forall a b. a -> (a -> b) -> b
& SymbolTableMappings -> HashMap Word ShortText
symbolsToText
          forall a b. a -> (a -> b) -> b
& forall k v. HashMap k v -> Int
HashMap.size
          forall a b. a -> (a -> b) -> b
& forall a b. (Integral a, Num b) => a -> b
fromIntegral
  forall (f :: * -> *) a. Applicative f => a -> f a
pure Word
size

finalizer :: Word -> IO ()
finalizer :: Word -> IO ()
finalizer Word
idx = do
  forall a b. MVar a -> (a -> IO b) -> IO b
MVar.withMVar (GlobalSymbolTable -> MVar Word
next GlobalSymbolTable
globalSymbolTable') forall a b. (a -> b) -> a -> b
$ \Word
_next -> do
    forall a. IORef a -> (a -> a) -> IO ()
IORef.modifyIORef' (GlobalSymbolTable -> IORef SymbolTableMappings
mappings GlobalSymbolTable
globalSymbolTable') forall a b. (a -> b) -> a -> b
$ \SymbolTableMappings {HashMap Word ShortText
symbolsToText :: HashMap Word ShortText
symbolsToText :: SymbolTableMappings -> HashMap Word ShortText
symbolsToText, Map ShortText (Weak Symbol)
textToSymbols :: Map ShortText (Weak Symbol)
textToSymbols :: SymbolTableMappings -> Map ShortText (Weak Symbol)
textToSymbols} ->
      case forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HashMap.lookup Word
idx HashMap Word ShortText
symbolsToText of
        Maybe ShortText
Nothing -> forall a. HasCallStack => [Char] -> a
error ([Char]
"Duplicate finalizer called for " forall a. Semigroup a => a -> a -> a
<> forall a. Show a => a -> [Char]
show Word
idx forall a. Semigroup a => a -> a -> a
<> [Char]
"This should never happen") -- SymbolTableMappings {symbolsToText, textToSymbols}
        Just ShortText
text ->
          SymbolTableMappings
            { symbolsToText :: HashMap Word ShortText
symbolsToText = forall k v. (Eq k, Hashable k) => k -> HashMap k v -> HashMap k v
HashMap.delete Word
idx HashMap Word ShortText
symbolsToText,
              textToSymbols :: Map ShortText (Weak Symbol)
textToSymbols = forall k a. Ord k => k -> Map k a -> Map k a
Map.delete ShortText
text Map ShortText (Weak Symbol)
textToSymbols
            }
{-# NOINLINE finalizer #-}