packages feed

tilia-0.1.2.0: src/Tilia/Memo.hs

{-# LANGUAGE LambdaCase #-}

-- | Tables of answers, each worked out once however many threads ask for it.
module Tilia.Memo
  ( Memo,
    newMemo,
    Flights,
    newFlights,
    memoized,
  )
where

import Control.Concurrent
  ( MVar,
    ThreadId,
    modifyMVar,
    modifyMVar_,
    myThreadId,
    newEmptyMVar,
    newMVar,
    readMVar,
    tryPutMVar,
    tryReadMVar,
  )
import Control.Exception (onException)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Unique (Unique, newUnique)

-- | Answers filed under the names they are about, each worked out once.
data Memo v = Memo Unique (IORef (Map Text (MVar v)))

-- | An empty 'Memo'.
newMemo :: IO (Memo v)
newMemo = Memo <$> newUnique <*> newIORef Map.empty

-- | Which thread is working out which answer, and which answer each waiting
-- thread waits for, across all of a resolver's tables.
data Flights = Flights
  { flightOwners :: Map (Unique, Text) ThreadId,
    flightWaits :: Map ThreadId (Unique, Text)
  }

-- | Nobody working out anything, and nobody waiting.
newFlights :: IO (MVar Flights)
newFlights = newMVar (Flights Map.empty Map.empty)

-- | Look an answer up in a table, working it out and filing it the first
-- time.
memoized ::
  -- | Who is working out what.
  MVar Flights ->
  -- | Where the answers are filed.
  Memo v ->
  -- | The answer for a thread that cannot wait.
  v ->
  -- | What the answer is about.
  Text ->
  -- | Work that is being memoized.
  IO v ->
  IO v
memoized flights (Memo table answers) standIn key work =
  Map.lookup key <$> readIORef answers >>= \case
    Just slot -> tryReadMVar slot >>= maybe (waitFor slot) pure
    Nothing -> do
      me <- myThreadId
      claimed <- modifyMVar flights $ \fl -> do
        filed <- readIORef answers
        case Map.lookup key filed of
          Just slot -> pure (fl, Left slot)
          Nothing -> do
            slot <- newEmptyMVar
            writeIORef answers (Map.insert key slot filed)
            pure
              ( fl{flightOwners = Map.insert (table, key) me (flightOwners fl)},
                Right slot
              )
      case claimed of
        Left slot -> waitFor slot
        Right slot -> do
          answer <- work `onException` file slot standIn
          answer <$ file slot answer
  where
    file slot answer = modifyMVar_ flights $ \fl -> do
      _ <- tryPutMVar slot answer
      pure fl{flightOwners = Map.delete (table, key) (flightOwners fl)}
    waitFor slot = do
      me <- myThreadId
      settled <- modifyMVar flights $ \fl ->
        tryReadMVar slot >>= \case
          Just answer -> pure (fl, Just answer)
          Nothing
            | waitedOnBy fl me (table, key) -> pure (fl, Just standIn)
            | otherwise ->
                pure
                  ( fl{flightWaits = Map.insert me (table, key) (flightWaits fl)},
                    Nothing
                  )
      case settled of
        Just answer -> pure answer
        Nothing -> do
          answer <- readMVar slot
          modifyMVar_ flights $ \fl ->
            pure fl{flightWaits = Map.delete me (flightWaits fl)}
          pure answer

-- | Is this answer being worked out by the given thread, or by one that
-- waits for it, directly or through others?
waitedOnBy :: Flights -> ThreadId -> (Unique, Text) -> Bool
waitedOnBy fl me = go
  where
    go key = case Map.lookup key (flightOwners fl) of
      Nothing -> False
      Just owner
        | owner == me -> True
        | otherwise -> maybe False go (Map.lookup owner (flightWaits fl))