packages feed

salmon-ops-0.1.0.0: src/Salmon/Actions/Dot.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Salmon.Actions.Dot (
    printDigraph,
    printCograph,
    printDagCograph,
    PlaceHolder (..),
    OpaqueNode (..),
    DotGraphExt,
) where

import Control.Comonad.Cofree (Cofree (..))
import Data.Foldable (toList, traverse_)
import GHC.Records

import Data.Dynamic (Dynamic, fromDynamic)
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import qualified Data.Maybe as Maybe
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text

import Salmon.FoldBranch
import Salmon.Op.Actions
import Salmon.Op.Dag (Dag)
import qualified Salmon.Op.Dag as Dag
import Salmon.Op.Eval
import Salmon.Op.Graph
import Salmon.Op.GraphFold (Branch (..), Shape (..), foldWithContext)
import Salmon.Op.OpGraph
import Salmon.Op.Ref

type Node ext = Maybe (Ref, ShortHand, ext)

type RawEdge = (Ref, Ref)

data Edge = Edge {connection :: ConnectType, rawEdge :: RawEdge}
data PrevConnectType = CL | CR | OV
data CurConnectType = V | O | C
type ConnectType = (PrevConnectType, CurConnectType)

-- | Slightly over-constrained constraint for graph extentions we want to represent.
type DotGraphExt ext = (HasField "dynamics" ext [Dynamic], HasField "ref" ext Ref)

data PlaceHolder = PlaceHolder Text

data OpaqueNode = OpaqueNode Text

hasPlaceholder ::
    (DotGraphExt ext) =>
    ext ->
    Bool
hasPlaceholder e =
    not $ null $ Maybe.catMaybes $ map (fromDynamic @PlaceHolder) e.dynamics

hasOpaqueNode ::
    (DotGraphExt ext) =>
    ext ->
    Bool
hasOpaqueNode e =
    not $ null $ Maybe.catMaybes $ map (fromDynamic @OpaqueNode) e.dynamics

dotNode :: (DotGraphExt ext) => Node ext -> Text
dotNode Nothing = ""
dotNode (Just (ref, name, ext))
    | hasOpaqueNode ext = mconcat [unRef ref, "[color=darkgreen;shape=egg;label=\"", dotEscape name, "\"];"]
    | hasPlaceholder ext = mconcat [unRef ref, "[color=grey;label=\"", dotEscape name, "\"];"]
    | otherwise = mconcat [unRef ref, "[label=\"", dotEscape name, "\"];"]

dotEdge :: Edge -> Text
dotEdge e =
    let (ref1, ref2) = e.rawEdge
     in case e.connection of
            (OV, _) -> mconcat [unRef ref1, "->", unRef ref2]
            (CL, _) -> mconcat [unRef ref1, "->", unRef ref2, "[color=red]"]
            (CR, C) -> mconcat [unRef ref1, "->", unRef ref2, "[color=orange]"]
            (CR, O) -> mconcat [unRef ref1, "->", unRef ref2, "[color=gray]"]
            (CR, V) -> mconcat [unRef ref1, "->", unRef ref2]

dotEscape :: Text -> Text
dotEscape = id

sameNode :: Node a -> Node a -> Bool
sameNode n1 n2 = Maybe.fromMaybe False $ do
    (l, _, _) <- n1
    (r, _, _) <- n2
    pure $ l == r

sameEdge :: Edge -> Edge -> Bool
sameEdge e1 e2 = e1.rawEdge == e2.rawEdge

-------------------------------------------------------------------------------

evalEdges ::
    forall m ext.
    (DotGraphExt ext) =>
    Cofree Graph (OpGraph m (Actions ext)) ->
    [Edge]
evalEdges = foldWithContext Nothing onNode nextCtx . fmap node
  where
    onNode :: Maybe (PrevConnectType, ext) -> Shape -> Actions ext -> [Edge]
    onNode prev shape a =
        [Edge (ct, curOf shape) (l.ref, r.ref) | r <- toList a, (ct, l) <- toList prev]

    curOf :: Shape -> CurConnectType
    curOf SVertices = V
    curOf SOverlay = O
    curOf SConnect = C

    nextCtx :: Maybe (PrevConnectType, ext) -> Branch -> Actions ext -> Maybe (PrevConnectType, ext)
    nextCtx prev branch a = case a of
        Actionless -> prev
        Actions (Act _ ext) -> Just (branchToPrevCT branch, ext)

    branchToPrevCT :: Branch -> PrevConnectType
    branchToPrevCT FromVertices = OV
    branchToPrevCT FromOverlayL = OV
    branchToPrevCT FromOverlayR = OV
    branchToPrevCT FromConnectL = CL
    branchToPrevCT FromConnectR = CR

-------------------------------------------------------------------------------

evalNodes ::
    (DotGraphExt ext) =>
    Cofree Graph (OpGraph m (Actions ext)) ->
    Cofree Graph (Node ext)
evalNodes = fmap (\x -> mkNode x.node)

mkNode ::
    (DotGraphExt ext) =>
    Actions ext ->
    Node ext
mkNode x =
    case x of
        Actionless ->
            Nothing
        (Actions act) ->
            Just (act.extension.ref, act.shorthand, act.extension)

-------------------------------------------------------------------------------

printDigraph ::
    forall m ext.
    ( Monad m
    , DotGraphExt ext
    ) =>
    (forall a. m a -> IO a) ->
    OpGraph m (Actions ext) ->
    IO ()
printDigraph nat graph = do
    printCograph =<< nat (expand graph)

printCograph ::
    forall m ext.
    ( Monad m
    , DotGraphExt ext
    ) =>
    Cofree Graph (OpGraph m (Actions ext)) ->
    IO ()
printCograph gr1 = do
    let nodes = fmap dotNode $ List.nubBy sameNode $ toList $ evalNodes gr1
    let edges = fmap dotEdge $ List.nubBy sameEdge $ evalEdges gr1
    putStrLn "digraph {"
    putStrLn "rankdir=LR;"
    traverse_ Text.putStrLn $ nodes
    traverse_ Text.putStrLn $ edges
    putStrLn "}"

{- | (R4) 'printCograph' for a folded, and possibly rewritten, 'Dag' rather
than the declared @Cofree Graph@ — what @run dag@ prints once any
"Salmon.Op.Rewrite" phases are registered, so a batch is one node in the
picture rather than however many nodes it replaced.

A 'Dag' has already collapsed 'Connect'\/'Overlay' into plain dependency
edges, so the red\/orange\/gray distinction 'printCograph' draws from
'Salmon.Op.GraphFold.Shape' has nothing left to key off — every edge here is
"depends on", drawn the same way 'OV' edges always were.
-}
printDagCograph ::
    forall ext.
    (DotGraphExt ext) =>
    Dag ext ->
    IO ()
printDagCograph dag = do
    let nodes = fmap dotNode [mkDagNode aref act | (aref, act) <- Map.toList (Dag.dagNodes dag)]
    let edges =
            [ dotEdge (Edge (OV, V) (dref, aref))
            | aref <- Dag.dagOrder dag
            , dref <- Dag.dependenciesOf dag aref
            ]
    putStrLn "digraph {"
    putStrLn "rankdir=LR;"
    traverse_ Text.putStrLn $ nodes
    traverse_ Text.putStrLn $ edges
    putStrLn "}"
  where
    mkDagNode :: Ref -> Act ext -> Node ext
    mkDagNode aref act = Just (aref, act.shorthand, act.extension)