packages feed

debuggable-0.1.0: src/Debug/Provenance/Internal.hs

module Debug.Provenance.Internal (
    -- * Callsites
    CallSite(..)
  , prettyCallSite
  , briefSrcLoc
  , callSite
  , callSiteWithLabel
    -- * Invocations
  , Invocation -- opaque
  , prettyInvocation
  , newInvocation
  , newInvocationFrom
  ) where

import Control.Monad.IO.Class
import Data.Hashable
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.IORef
import Data.List (intercalate)
import Data.Maybe (fromMaybe)
import GHC.Generics
import GHC.Stack
import System.IO.Unsafe (unsafePerformIO)

{-------------------------------------------------------------------------------
  Callsites
-------------------------------------------------------------------------------}

-- | Callsite
--
-- A callsite tells you where something was called: a location in the source,
-- and the name of the function that did the calling. Optionally, they can be
-- given an additional user-defined label also.
--
-- /NOTE/: If you are seeing @{unknown}@ instead of the function name,
-- the calling function does not have a 'HasCallStack' annotation:
--
-- > yourFunction :: HasCallStack => IO () -- 'HasCallStack' probably missing
-- > yourFunction = do
-- >     let cs = callSite
-- >     ..
--
-- Once you add this annotation, you should see @yourFunction@ instead of
-- @{unknown}@. Similarly, if you have local function definitions, it may
-- be useful to give those 'HasCallStack' constraints of their own:
--
-- > yourFunction :: HasCallStack => IO ()
-- > yourFunction = ..
-- >   where
-- >     someLocalFn :: HasCallStack => IO ()
-- >     someLocalFn = do
-- >         let cs = callSite
-- >         ..
--
-- In this example the 'HasCallStack' constraint on @someLocalFn@ means that the
-- calling function will be reported as @someLocalFn@ instead of @yourFunction@.
data CallSite = CallSite {
      callSiteSrcLoc :: Maybe SrcLoc
    , callSiteCaller :: Maybe String
    , callSiteCallee :: Maybe String
    , callSiteLabel  :: Label
    }
  deriving stock (Eq)

instance Show CallSite where
  show = prettyCallSite

-- | Label associated with 'CallSite'
--
-- This is an internal type.
data Label = Label String | NoLabel
  deriving stock (Eq, Generic)
  deriving anyclass (Hashable)

-- | Render 'CallSite' to human-readable format
prettyCallSite :: CallSite -> String
prettyCallSite cs =
    concat [
        intercalate " -> " [
            fromMaybe "{unknown}" callSiteCaller
          , fromMaybe "{unknown}" callSiteCallee
          ]
      , " ("
      , intercalate ", " $ concat [
            [ briefSrcLoc loc
            | Just loc <- [callSiteSrcLoc]
            ]
          , [ show label
            | Label label <- [callSiteLabel]
            ]
          ]
      , ")"
      ]
  where
    CallSite{
        callSiteSrcLoc
      , callSiteCaller
      , callSiteCallee
      , callSiteLabel
      } = cs

-- | Variant on 'prettySrcLoc' which omits the package and module name
briefSrcLoc :: SrcLoc -> [Char]
briefSrcLoc loc = intercalate ":" [
      srcLocFile loc
    , show $ srcLocStartLine loc
    , show $ srcLocStartCol loc
    ]

instance Hashable CallSite where
  hashWithSalt salt cs =
      hashWithSalt salt (
          prettySrcLoc <$> callSiteSrcLoc
        , callSiteCaller
        , callSiteCallee
        , callSiteLabel
        )
    where
      CallSite{
          callSiteSrcLoc
        , callSiteCaller
        , callSiteCallee
        , callSiteLabel
        } = cs


-- | Current 'CallSite'
callSite :: HasCallStack => CallSite
callSite = withFrozenCallStack $ mkCallSite NoLabel

-- | Current 'CallSite' with user-defined label
callSiteWithLabel :: HasCallStack => String -> CallSite
callSiteWithLabel label = withFrozenCallStack $ mkCallSite (Label label)

-- | Internal auxiliary to 'callSite' and 'callSiteWithLabel'
mkCallSite  :: HasCallStack => Label -> CallSite
mkCallSite callSiteLabel = aux callStack
  where
    aux :: CallStack -> CallSite
    aux cs =
        -- drop the call to @callSite{withLabel}@
        case getCallStack cs of
          _ : (callee, loc) : [] -> CallSite {
              callSiteSrcLoc = Just loc
            , callSiteCaller = Nothing
            , callSiteCallee = Just callee
            , callSiteLabel
            }
          _ : (callee, loc) : (caller, _) : _ -> CallSite {
              callSiteSrcLoc   = Just loc
            , callSiteCaller = Just caller
            , callSiteCallee = Just callee
            , callSiteLabel
            }
          _otherwise -> CallSite {
              callSiteSrcLoc = Nothing
            , callSiteCaller = Nothing
            , callSiteCallee = Nothing
            , callSiteLabel
            }

{-------------------------------------------------------------------------------
  Invocations
-------------------------------------------------------------------------------}

-- | Invocation
--
-- An invocation not only tells you the /where/, but also the /when/: it pairs a
-- 'CallSite' with a count, automatically incremented on each call to
-- 'newInvocation'. Each 'CallSite' uses its own counter.
data Invocation = Invocation CallSite Int
  deriving stock (Eq)

instance Show Invocation where
  show = prettyInvocation

-- | Render 'Invocation' to human-readable format
prettyInvocation :: Invocation -> String
prettyInvocation (Invocation cs n) =
    concat [
        fromMaybe "{unknown}" callSiteCaller
      , " ("
      , intercalate ", " $ concat [
            [ intercalate ":" [
                  srcLocFile loc
                , show $ srcLocStartLine loc
                , show $ srcLocStartCol loc
                ]
            | Just loc <- [callSiteSrcLoc]
            ]
          , [ show label
            | Label label <- [callSiteLabel]
            ]
          ]
      , ") #"
      , show n
      ]
  where
    -- the callee is 'newInvocation'
    CallSite{
        callSiteSrcLoc
      , callSiteCaller
      , callSiteLabel
      } = cs

-- | New invocation
--
-- See 'Invocation' for discussion.
newInvocation :: (HasCallStack, MonadIO m) => m Invocation
newInvocation =
    -- We intentionally do /NOT/ freeze the callstack here: when function @foo@
    -- calls @newInvocation@, we want a 'CallSite' of @foo -> newInvocation@,
    -- not @bar -> foo@.
    newInvocationFrom callSite

-- | Generalization of 'newInvocation'
newInvocationFrom :: MonadIO m => CallSite -> m Invocation
newInvocationFrom cs = liftIO $ do
    atomicModifyIORef' globalCounters $ \counters ->
      let i = HashMap.findWithDefault 1 cs counters
      in (HashMap.insert cs (succ i) counters, Invocation cs i)

{-------------------------------------------------------------------------------
  Internal: globals
-------------------------------------------------------------------------------}

globalCounters :: IORef (HashMap CallSite Int)
{-# NOINLINE globalCounters #-}
globalCounters = unsafePerformIO $ newIORef HashMap.empty