packages feed

calligraphy-0.1.6: src/Calligraphy/Phases/Render/Common.hs

{-# LANGUAGE RecordWildCards #-}

-- | Prepare the call graph for rendering
module Calligraphy.Phases.Render.Common
  ( RenderConfig (..),
    pRenderConfig,
    renderGraph,
    RenderError,
    ppRenderError,
    ID,
    RenderGraph (..),
    RenderModule (..),
    RenderNode (..),
    ClusterModules (..),
    if',
  )
where

import Calligraphy.Util.Printer (Prints, strLn)
import Calligraphy.Util.Types (CallGraph (..), Decl (..), DeclType, GHCKey (unGHCKey), Key (..), Loc (..), Module (..))
import Control.Applicative ((<|>))
import Data.Bifunctor (bimap)
import qualified Data.EnumSet as EnumSet
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Data.Maybe (catMaybes, mapMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Tree (Tree)
import Options.Applicative (Parser, flag, flag', help, long)

data RenderConfig = RenderConfig
  { showCalls :: Bool,
    showTypes :: Bool,
    showKey :: Bool,
    showGHCKeys :: Bool,
    showModulePath :: Bool,
    locMode :: LocMode,
    clusterModules :: ClusterModules
  }

data ClusterModules
  = ClusterNever
  | ClusterWhenMultiple
  | ClusterAlways

pRenderConfig :: Parser RenderConfig
pRenderConfig =
  RenderConfig
    <$> flag True False (long "hide-calls" <> help "Don't show call arrows")
    <*> flag True False (long "hide-types" <> help "Don't show type arrows")
    <*> flag False True (long "show-key" <> help "Show internal keys with identifiers. Useful for debugging.")
    <*> flag False True (long "show-ghc-key" <> help "Show GHC keys with identifiers. Useful for debugging.")
    <*> flag False True (long "show-module-path" <> help "Show a module's filepath instead of its name")
    <*> pLocMode
    <*> pClusterModules

pClusterModules :: Parser ClusterModules
pClusterModules =
  flag' ClusterNever (long "no-cluster-modules" <> help "Don't draw modules as a cluster.")
    <|> flag' ClusterAlways (long "force-cluster-modules" <> help "Draw modules as a cluster, even if there is only one.")
    <|> pure ClusterWhenMultiple

-- | A directly printable string uniquely identifying a declaration.
type ID = String

-- | A representation of the call graph that's convenient for rendering.
-- Structurally, it's the same as 'CallGraph', in that it's a tree of nodes and a flat list of edges.
-- The differences is that as much of the non-backend-specific preprocessing has already been taken care of.
--   - Nodes and modules have a unique string ID
--   - Nodes and modules contain their desired label
--   - Render roots are guaranteed to be non-empty
--   - Set of calls and types are empty on --hide-{calls, types}
--   - Modules are flattened depending on --no-cluster-modules
data RenderGraph = RenderGraph
  { renderRoots :: Either (NonEmpty RenderModule) (NonEmpty (Tree RenderNode)), -- Right if --no-cluster-modules
    callEdges :: Set (ID, ID), -- empty if --hide-calls
    typeEdges :: Set (ID, ID) -- empty if --hide-types
  }

data RenderModule = RenderModule
  { moduleLabel :: String,
    moduleId :: ID,
    moduleDecls :: NonEmpty (Tree RenderNode)
  }

data RenderNode = RenderNode
  { nodeId :: ID,
    nodeType :: DeclType,
    nodeLabelLines :: [String],
    nodeExported :: Bool
  }

data LocMode = Hide | Line | LineCol

data RenderError = EmptyGraph

ppRenderError :: Prints RenderError
ppRenderError EmptyGraph = strLn "Output graph is empty"

renderGraph :: RenderConfig -> CallGraph -> Either RenderError RenderGraph
renderGraph
  RenderConfig {..}
  (CallGraph modules calls types) =
    case nonEmpty (mapMaybe (uncurry mkModule) (zip modules [0 ..])) of
      Nothing -> Left EmptyGraph
      Just neModules ->
        pure $
          RenderGraph
            ( let cluster = case clusterModules of
                    ClusterAlways -> True
                    ClusterWhenMultiple -> length neModules > 1
                    ClusterNever -> False
               in if cluster then Left neModules else Right (neModules >>= moduleDecls)
            )
            (if showCalls then Set.map (bimap keyId keyId) calls else Set.empty)
            (if showTypes then Set.map (bimap keyId keyId) types else Set.empty)
    where
      keyId :: Key -> ID
      keyId (Key k) = "node_" <> show k

      mkModule :: Module -> Int -> Maybe RenderModule
      mkModule (Module name path decls) ix =
        (\ne -> RenderModule label ("module_" <> show ix) (fmap mkNode <$> ne)) <$> nonEmpty decls
        where
          label
            | showModulePath = path
            | otherwise = name

      mkNode :: Decl -> RenderNode
      mkNode (Decl name key ghcKeys x t loc) = RenderNode (keyId key) t (catMaybes lbls) x
        where
          lbls =
            [ pure name,
              if' showKey $ show (unKey key),
              if' showGHCKeys $ "GHC Keys: " <> (unwords . fmap (show . unGHCKey) . EnumSet.toList $ ghcKeys),
              case locMode of
                Hide -> Nothing
                Line -> Just ('L' : show (locLine loc))
                LineCol -> Just (show loc)
            ]

pLocMode :: Parser LocMode
pLocMode =
  flag' Line (long "show-line" <> help "Show line numbers")
    <|> flag' LineCol (long "show-line-col" <> help "Show line and column numbers")
    <|> pure Hide

-- TODO this needs to be moved to an appropriate Util/Lib module
if' :: Bool -> a -> Maybe a
if' True a = Just a
if' False _ = Nothing