packages feed

registry-0.3.2.1: src/Data/Registry/Internal/Dot.hs

-- |
--  Nested datatype to track the resolution algorithm
--
--  From this data type we can draw a graph of the full
--  instantiation of a value
module Data.Registry.Internal.Dot where

import Data.Hashable
import Data.List (elemIndex)
import Data.Map.Strict hiding (adjust)
import Data.Registry.Internal.Statistics
import Data.Registry.Internal.Types
import Data.Text as T
import Protolude as P
import Type.Reflection

-- | Make a list of graph edges from the list of function applications
makeEdges :: Operations -> [(Value, Value)]
makeEdges [] = []
makeEdges (AppliedFunction out ins : rest) = ((out,) <$> ins) <> makeEdges rest

-- * DOT GRAPH

-- | A DOT graph
newtype Dot = Dot
  { unDot :: Text
  }
  deriving (Eq, Show)

-- | Use a State type to get the current index of a value
-- when there are values of the same type and different
-- hash values
type DotState = State ValuesByType

-- | List of value hashes by value type
type ValuesByType = Map SomeTypeRep ValueHashes

-- | Type alias for a Hash
type Hash = Int

-- | Type alias for a ValueId
type ValueId = Int

-- | Type alias for a list of hashes
type ValueHashes = [Hash]

-- | Type alias for a list of an edge in the graph
type Edge = (Value, Value)

-- | Type alias for a list of edges
type Edges = [Edge]

-- | Type alias for associating a number to a value
type ValueCounter = Maybe Int

-- | Make a DOT graph out of all the function applications
toDot :: Operations -> Dot
toDot op =
  let edges = makeEdges op
      allValues = edges >>= (\(v1, v2) -> [v1, v2])
      valueTypes = execState (traverse countValueTypes allValues) mempty
   in Dot $
        T.unlines $
          [ "strict digraph {",
            "  node [shape=record]"
          ]
            <> (toDotEdge valueTypes <$> edges)
            <> ["}"]

-- | Update a map classifying values by type
countValueTypes :: Value -> DotState ()
countValueTypes value = do
  maps <- get
  let key = valueDynTypeRep value
  let valueHash = hashOf value

  case lookup key maps of
    -- there were no values for that type, create a list with the value hash
    Nothing -> put $ insert key [valueHash] maps
    -- there is a list of hashes for that type
    Just hashes ->
      case elemIndex valueHash hashes of
        -- that value hasn't been seen before
        Nothing -> do
          let newHashes = hashes <> [valueHash]
          put $ insert key newHashes maps

        -- the value has been seen before
        Just _ -> pure ()

-- | A DOT edge representing the dependency between 2 values
toDotEdge :: ValuesByType -> (Value, Value) -> Text
toDotEdge valuesByType (value1, value2) =
  let v1 = toDotVertex valuesByType value1
      v2 = toDotVertex valuesByType value2
   in v1 <> " -> " <> v2 <> ";"

-- | Represent a value as a vertex in a dot graph
--   we use some state to keep track of values of the
--   same type
--   The values are numbered starting from 1 when there are
--   several of them for the same type
toDotVertex :: ValuesByType -> Value -> Text
toDotVertex valuesByType value =
  let key = valueDynTypeRep value
      valueHash = hashOf value

      valueCounter =
        case lookup key valuesByType of
          Nothing -> Nothing -- this case should not happen given how the map is built
          Just hashes ->
            case hashes of
              [_] -> Nothing
              _ -> (+ 1) <$> elemIndex valueHash hashes
   in adjust (nodeDescription (valDescription value) valueCounter)

-- | Return the hash of a value based on its dependencies
hashOf :: Value -> Int
hashOf value =
  hash
    (unDependencies . valDependencies $ value, valDescription value)

-- | Description of a Value in the DOT graph
nodeDescription :: ValueDescription -> ValueCounter -> Text
nodeDescription (ValueDescription t Nothing) n =
  t <> showValueCounter n
nodeDescription (ValueDescription t (Just v)) n =
  nodeDescription (ValueDescription t Nothing) n <> "\n" <> v

-- | Don't show the counter if there
showValueCounter :: ValueCounter -> Text
showValueCounter Nothing = ""
showValueCounter (Just n) = "-" <> show n

-- | We need to process the node descriptions
--     - we add quotes arountd the text
--     - we remove quotes (") inside the text
--     - we escape newlines
adjust :: Text -> Text
adjust node = "\"" <> (escapeNewlines . removeQuotes) node <> "\""

-- | Remove quotes from a textual description to avoid breaking the DOT format
removeQuotes :: Text -> Text
removeQuotes = T.replace "\"" ""

-- | Replace \n with \\n so that newlines are kept in
--   node descriptions
escapeNewlines :: Text -> Text
escapeNewlines = T.replace "\n" "\\n"