{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-} module CHR.Language.Examples.Term.Visualizer ( chrVisualize ) where import Prelude import Data.Maybe import Data.List import qualified Data.Map as Map import CHR.Pretty import qualified CHR.Data.Lookup as Lk import CHR.Types import CHR.Types.Rule import CHR.Language.GTerm.Parser -- import UHC.Util.CHR.Solve.TreeTrie.Mono import CHR.Solve.MonoBacktrackPrio as MBP import CHR.Language.Examples.Term.AST -- import UHC.Util.CHR.Solve.TreeTrie.Internal -- import UHC.Util.CHR.Solve.TreeTrie.Internal.Shared import CHR.Data.Substitutable import Data.Graph.Inductive.Graph import Data.Graph.Inductive.Tree sortGroupOn :: Ord b => (a -> b) -> [a] -> [[a]] sortGroupOn f = construct . sortOn f where construct [] = [] construct (y:ys) = group : construct rest where group = y : takeWhile ((f y ==) . f) ys rest = dropWhile ((f y ==) . f) ys data NodeData -- Applied rule with first alt (if it exists) = NodeRule { nrLayer :: Int , nrColumn :: Int , nrName :: String , nrRuleVars :: [Tm] , nrFirstAlt :: Maybe C } -- Additional alts of a rule | NodeAlt { naLayer :: Int , naColumn :: Int , naConstraint :: C } -- Added node to make a proper layered graph -- A proper layered graph is a graph in which all edges -- go from a layer to the next layer. To satisfy this, -- we add synthesized nodes on edges that do not skip one -- or more layers | NodeSynthesized { nsLayer :: Int , nsColumn :: Int , nsEdgeKind :: EdgeKind } data EdgeKind = EdgeGuard -- Usage of term in guard of rule. | EdgeHead -- Usage of term in head of rule. | EdgeUnify -- Usage of some term that required unification of this node. | EdgeAlt -- Link between NodeRule and NodeAlt. Both nodes have same layer. deriving Eq type Node' = LNode NodeData -- | Edge has a kind and a bool that says whether this edge is -- the last edge of a sequence of edges. The last edge does not -- end in a synthesized node, the others do. type Edge' = LEdge (EdgeKind, Bool) type NodeEdge = (Node', Node', EdgeKind, Bool) asEdge :: NodeEdge -> Edge' asEdge ((from, _), (to, _), kind, isLast) = (from, to, (kind, isLast)) -- | Gets the layer of a node nodeLayer :: Node' -> Int nodeLayer (_, NodeRule{nrLayer = layer}) = layer nodeLayer (_, NodeAlt{naLayer = layer}) = layer nodeLayer (_, NodeSynthesized{nsLayer = layer}) = layer -- | Gets the column of a node nodeColumn :: Node' -> Int nodeColumn (_, NodeRule{nrColumn = col}) = col nodeColumn (_, NodeAlt{naColumn = col}) = col nodeColumn (_, NodeSynthesized{nsColumn = col}) = col -- | Sets the column of a node nodeSetColumn :: Node' -> Int -> Node' nodeSetColumn (n, d@NodeRule{}) col = (n, d{nrColumn = col}) nodeSetColumn (n, d@NodeAlt{}) col = (n, d{naColumn = col}) nodeSetColumn (n, d@NodeSynthesized{}) col = (n, d{nsColumn = col}) -- | A map between a term, and the location where it was found combined -- with the required unifications type NodeMap = Map.Map Tm (Node', [Node']) -- | Contains all data needed to build the graph, during traversal of -- the solve trace data BuildState = BuildState [Node'] [NodeEdge] NodeMap Int Int emptyBuildState :: BuildState emptyBuildState = BuildState [] [] Map.empty 0 0 -- | Gives all terms that follow after a unification replaceInTm :: Tm -> Tm -> Tm -> [Tm] replaceInTm a b tm | tm == a || tm == b = [a, b] | otherwise = case tm of Tm_Con name tms -> fmap (Tm_Con name) (replaceList tms) Tm_Lst tms ltm -> do tms' <- replaceList tms ltm' <- replaceMaybe ltm return $ Tm_Lst tms' ltm' Tm_Op op tms -> fmap (Tm_Op op) (replaceList tms) x -> [x] where replaceList = sequence . fmap (replaceInTm a b) replaceMaybe Nothing = [Nothing] replaceMaybe (Just y) = fmap Just $ replaceInTm a b y -- | Gives all terms in a constraint tmsInC :: C -> [Tm] tmsInC (C_Con s tms) = [Tm_Con s tms] tmsInC _ = [] -- | Gives all terms in a guard tmsInG :: G -> [Tm] tmsInG (G_Tm tm) = tmsInTm tm tmsInG _ = [] tmsInTm :: Tm -> [Tm] tmsInTm tm = tm : children tm where children (Tm_Lst as Nothing) = as children (Tm_Lst as (Just a)) = as ++ [a] children _ = [] -- | Finds all terms that were used for this rule -- Used by visualizer to draw edges to the origin of -- these rules. precedentTms :: Rule C G P P -> [(Tm, EdgeKind)] precedentTms rule = fmap (\n -> (n, EdgeHead)) (concatMap tmsInC $ ruleHead rule) ++ fmap (\n -> (n, EdgeGuard)) (concatMap tmsInG $ ruleGuard rule) -- | Adds the constraint (of an alt) to the NodeMap addConstraint :: C -> Node' -> NodeMap -> NodeMap addConstraint (CB_Eq a b) = addUnify a b addConstraint (C_Con s tms) = addTerm $ Tm_Con s tms addConstraint c = const id addTerm :: Tm -> Node' -> NodeMap -> NodeMap addTerm tm node = Map.insert tm (node, []) addUnify :: Tm -> Tm -> Node' -> NodeMap -> NodeMap addUnify a b node map = Map.foldlWithKey cb map map where cb :: NodeMap -> Tm -> (Node', [Node']) -> NodeMap cb map' tm (n, nodes) = foldl (\map'' key -> Map.insertWith compare key (n, node : nodes) map'') map' (replaceInTm a b tm) compare x@(_, nodes1) y@(_, nodes2) | length nodes1 <= length nodes2 = x | otherwise = y -- | Generates nodes and edges for a SolveStep. -- Stores the resulting terms in the NodeMap. stepToNodes :: BuildState -> SolveStep' C (MBP.StoredCHR C G P P) S -> BuildState stepToNodes state@(BuildState _ _ nodeMap nodeId layer) step = BuildState nodes edges'' nodeMap' nodeId' layer' where schr = stepChr step rule = storedChrRule' schr updRule = varUpd (stepSubst step) rule alt = maybe [] (fmap $ varUpd $ stepSubst step) $ stepAlt step (BuildState nodes edges' nodeMap' nodeId' layer', primaryNode) = createNodes (maybe "[untitled]" id (ruleName rule)) (Lk.elems (stepSubst step)) alt state edges'' = ( fmap (\(n, kind) -> (n, primaryNode, kind, True)) $ concatMap (\(n, ns, kind) -> (n, kind) : fmap (\x -> (x, EdgeUnify)) ns) $ mapMaybe (\(tm, kind) -> fmap (\(n, ns) -> (n, ns, kind)) (Map.lookup tm nodeMap)) (precedentTms updRule) ) ++ edges' createNodes :: String -> [Tm] -> [C] -> BuildState -> (BuildState, Node') createNodes name vars alts (BuildState previousNodes previousEdges nodeMap nodeId layer) = ( BuildState (nodes ++ previousNodes) (edges ++ previousEdges) nodeMap' (nodeId + max 1 (length alts)) (layer + 1) , primaryNode ) where primaryNode = (nodeId, NodeRule { nrLayer = layer , nrColumn = 0 , nrName = name , nrRuleVars = vars , nrFirstAlt = listToMaybe alts } ) nodes = primaryNode : altNodes altTms = concatMap tmsInC alts nodeMap' = foldl updateMap nodeMap nodes -- Updates node map for a new node updateMap :: NodeMap -> Node' -> NodeMap updateMap map node@(_, NodeRule{ nrFirstAlt = Just alt }) = addConstraint alt node map updateMap map node@(_, NodeAlt{ naConstraint = alt }) = addConstraint alt node map updateMap map _ = map altNode (constraint, i) = (nodeId + i, NodeAlt layer 0 constraint) altNodes = fmap altNode (drop 1 $ addIndices alts) edges = (fmap (\n -> (primaryNode, n, EdgeAlt, True)) altNodes) -- | Adds synthesized nodes to create a proper layered graph createSynthesizedNodes :: [Node'] -> [NodeEdge] -> Int -> ([NodeEdge], [Node']) createSynthesizedNodes nodes es firstNode = create es firstNode [] [] where create :: [NodeEdge] -> Int -> [NodeEdge] -> [Node'] -> ([NodeEdge], [Node']) create ((edge@(from, to, kind, _)):edges) id accumEdges accumNodes = create edges id' (es ++ accumEdges) (ns ++ accumNodes) where (es, ns, id') = split (nodeLayer from) edge id create _ _ accumEdges accumNodes = (accumEdges, accumNodes) split :: Int -> NodeEdge -> Int -> ([NodeEdge], [Node'], Int) split fromLayer edge@(from, to, kind, _) id | fromLayer + 1 >= nodeLayer to = ([edge], [], id) | otherwise = ( (from, node, kind, False) : edges', node : nodes', id' ) where node = (id, (NodeSynthesized (fromLayer + 1) 0 kind)) (edges', nodes', id') = split (fromLayer + 1) (node, to, kind, True) (id + 1) -- | Creates a graph with the visualization createGraph :: [C] -> [SolveStep' C (MBP.StoredCHR C G P P) S] -> Gr NodeData (EdgeKind, Bool) createGraph query steps = mkGraph sortedLayers (fmap asEdge edges) where -- | Sort the layers by giving each node in a layer an unique nodeColumn value sortedLayers = sortedFirstLayer ++ sortNodes maxLayerSize (sortedFirstLayer : layers) layeredEdges -- | Set the nodeColumn values of each of the nodes in the query (the query forms the first layer) sortedFirstLayer = uniqueColumns firstLayer ((maxLayerSize - length firstLayer) `div` 2) -- | Extracting [[Node']] from layerNodes firstLayer : layers = sortGroupOn nodeLayer nodes -- firstLayer : layers = Map.elems $ layerNodes nodes -- | For each layer we create a list with the nodes in that layer -- layerNodes :: [Node'] -> Map.Map Int [Node'] -- layerNodes ns = foldl (\m x -> Map.insertWith (++) (nodeLayer x) [x] m) Map.empty ns (state, _) = createNodes "?" [] query emptyBuildState BuildState nodes' edges' _ id _ = foldr (flip stepToNodes) state steps (edges, synNodes) = createSynthesizedNodes nodes' edges' id nodes = nodes' ++ synNodes maxLayerSize = maximum $ fmap length (firstLayer : layers) edgesCrossLayer = filter (\(from, to, _, _) -> nodeLayer from /= nodeLayer to) edges layeredEdges = sortGroupOn (nodeLayer . fst') edgesCrossLayer -- | Sort the nodes using the median heuristic -- | The first layer is left as it was, the second layer is sorted using the first etc. sortNodes :: Int -> [[Node']] -> [[NodeEdge]] -> [Node'] sortNodes _ (x:[]) _ = [] sortNodes maxLayerSize (x:xs:xss) e = medianHeurstic maxLayerSize x xs edges ++ sortNodes maxLayerSize (xs:xss) rest where (edges, rest) = if null e then ([], []) else if (nodeLayer $ fst' $ head $ head e) == nodeLayer (head x) then (head e, tail e) else ([], e) -- | lowerLayer is the layer to be sorted, upperLayer is assumed to be sorted -- | The maxLayerSize is used to center the graph (by altering the value given to uniqueColumns) -- | Documentation for the median heuristic: -- | https://cs.brown.edu/~rt/gdhandbook/chapters/hierarchical.pdf -- | http://www.cs.usyd.edu.au/~shhong/fab.pdf -- | https://books.google.nl/books?id=6hfsCAAAQBAJ&lpg=PA28&dq=median%20heuristic%20sorting%20vertices&hl=nl&pg=PA28#v=onepage&q&f=false medianHeurstic :: Int -> [Node'] -> [Node'] -> [NodeEdge] -> [Node'] medianHeurstic maxLayerSize upperLayer lowerLayer e = uniqueColumns sortedMedianList ((maxLayerSize - length lowerLayer) `div` 2) where -- | The medianList sorted on the median values sortedMedianList = sortOn nodeColumn medianList -- | The list of median values for each of the nodes in lowerLayer medianList = map (\x -> nodeSetColumn x (median x)) lowerLayer -- | The median value of the x coördinates of the neighbors median n | neighborCount == 0 = 0 | otherwise = coords !! (ceiling (realToFrac neighborCount / 2) - 1) where coords = coordinates n neighborCount = length coords -- | The values of the x coördinates of the neighbors coordinates n = map nodeColumn (neighbors n) -- | The neighbor nodes of the given Node' n (on a higher layer) neighbors n = map (fst') (edges n) -- | All the edges connected to given Node' n edges n = filter (\(_, (id, _), _, _) -> id == fst n) e -- | Ensure that each Node' has an unique nodeColumn (the x coördinate) -- | The value of the nodeColumn is set to i uniqueColumns :: [Node'] -> Int -> [Node'] uniqueColumns (n:ns) i = nodeSetColumn n i : uniqueColumns ns (i + 1) uniqueColumns _ _ = [] fst' :: (a, b, c, d) -> a fst' (a, _, _, _) = a -- | Creates a HTML tag tag :: String -> PP_Doc -> PP_Doc -> PP_Doc tag name attr content = (text ("<" ++ name)) >|< attributes attr >|< body content where attributes Emp = Emp attributes a = text " " >|< a body Emp = text " />" body content = text ">" >|< content >|< text ("") -- | Creates a HTML tag without attributes tag' :: String -> PP_Doc -> PP_Doc tag' name = tag name Emp -- | Add indices to an array as a tuple with value and index addIndices :: [a] -> [(a, Int)] addIndices = flip zip [0..] -- | Generates HTML for a node showNode :: (Node' -> (Int, Int)) -> Node' -> PP_Doc showNode pos node@(_, NodeRule{nrLayer = layer, nrName = name, nrRuleVars = vars, nrFirstAlt = alt}) = tag "div" ( text "class=\"rule\" style=\"top: " >|< pp (y + 10) >|< text "px; left: " >|< pp x >|< text "px;\"" ) ( tag "span" (text "class=\"" >|< className >|< text "\"") ( (text name) >|< (hlist (fmap ((" " >|<) . pp) vars)) ) >|< tag' "br" Emp >|< text "↳" >|< tag "span" (text "class=\"rule-alt\"") altText ) where (x, y) = pos node altText = maybe (text ".") pp alt className = text "rule-text" showUsage name var = tag "div" (text $ "class=\"" ++ className ++ "\"") (text " ") where className = name ++ " var-" ++ var showNode pos node@(_, NodeAlt{ naConstraint = constraint }) = tag "div" ( text "class=\"rule-additional-alt\" style=\"top: " >|< pp (y + 10) >|< text "px; left: " >|< pp x >|< text "px;\"" ) ( text "↳" >|< tag "span" (text "class=\"rule-alt\"") (pp constraint) ) where (x, y) = pos node showNode _ (_, NodeSynthesized{}) = Emp -- | Generates HTML for an edge showEdge :: (Node -> (Int, Int)) -> Edge' -> PP_Doc showEdge pos (from, to, (kind, isEnd)) = if kind == EdgeAlt then -- Edge between rule and alt of same rule tag "div" ( text "class=\"edge-alt\" style=\"top: " >|< pp y1 >|< "px; left: " >|< pp (min x1 x2) >|< "px; width: " >|< abs (x2 - x1 - 16) >|< "px;\"" ) (text " ") else tag "div" ( text "class=\"edge-ver " >|< text className >|< text "\" style=\"top: " >|< pp (y1 + 35) >|< "px; left: " >|< pp x1 >|< "px; height: " >|< (y2 - y1 - 60 - 6) >|< "px;\"" ) (text " ") >|< tag "div" ( text "class=\"edge-hor" >|< text (if x2 > x1 then " edge-hor-left " else if x2 < x1 then " edge-hor-right " else " edge-hor-no-curve ") >|< text className >|< text "\" style=\"top: " >|< pp (y2 - 19) >|< "px; left: " >|< pp (if x1 < x2 then x1 else x2 + (if isEnd then 0 else (abs (x2 - x1) + 1) `div` 2)) >|< "px; width: " >|< pp (abs (x2 - x1) `div` (if isEnd then 1 else 2)) >|< "px;\"" ) (text " ") >|< (if isEnd then Emp else tag "div" ( text "class=\"edge-end edge-end-" >|< text (if x2 > x1 then "left " else if x2 < x1 then "right " else "no-curve ") >|< text className >|< text "\" style=\"top: " >|< pp (y2 - 3 + 11) >|< "px; left: " >|< pp (if x1 < x2 then (x1 + x2) `div` 2 + 6 else x2) >|< pp "px; width: " >|< pp (if x1 == x2 then 0 else ((abs (x2 - x1) + 1) `div` 2) - 6) >|< "px;\"" ) (text " ") ) where (x1, y1) = pos from (x2, y2) = pos to className = case kind of EdgeAlt -> "" EdgeGuard -> "edge-guard" EdgeHead -> "edge-head" EdgeUnify -> "edge-unify" -- | Creates a visualization for the given query and solve trace. -- Output is a PP_Doc containing a HTML file. chrVisualize :: [C] -> SolveTrace' C (MBP.StoredCHR C G P P) S -> PP_Doc chrVisualize query trace = tag' "html" $ tag' "head" ( tag' "title" (text "CHR visualization") >|< tag' "style" styles ) >|< tag' "body" ( body ) where graph = createGraph query trace body = ufold reduce Emp graph >|< hlist (fmap (showEdge posId) $ labEdges graph) reduce (inn, id, node, out) right = showNode pos (id, node) >|< right nodeCount = length $ nodes graph pos :: Node' -> (Int, Int) pos n = ((nodeColumn n) * 200, (nodeLayer n) * 60) posId :: Node -> (Int, Int) posId node = pos (node, fromJust $ lab graph node) -- | The stylesheet used in the visualization. styles :: PP_Doc styles = text "body {\n\ \ font-size: 9pt;\n\ \ font-family: Arial;\n\ \}\n\ \.rule {\n\ \ position: absolute;\n\ \ white-space: nowrap;\n\ \}\n\ \.rule-text {\n\ \ border: 1px solid #aaa;\n\ \ background-color: #fff;\n\ \ display: inline-block;\n\ \ padding: 2px;\n\ \ margin: 3px 1px 0;\n\ \ min-width: 30px;\n\ \ text-align: center;\n\ \}\n\ \.rule-alt {\n\ \ display: inline-block;\n\ \ color: #A89942;\n\ \ background: #fff;\n\ \}\n\ \.rule-additional-alt {\n\ \ position: absolute;\n\ \ white-space: nowrap;\n\ \ margin-top: 24px;\n\ \}\n\ \.edge-ver {\n\ \ position: absolute;\n\ \ width: 0px;\n\ \ border-left: 6px solid #578999;\n\ \ opacity: 0.4;\n\ \ margin-left: 15px;\n\ \ margin-top: 9px;\n\ \ z-index: -1;\n\ \}\n\ \.edge-hor {\n\ \ position: absolute;\n\ \ height: 27px;\n\ \ border-bottom: 6px solid #578999;\n\ \ opacity: 0.4;\n\ \ margin-left: 15px;\n\ \ margin-top: 8px;\n\ \ z-index: -1;\n\ \}\n\ \.edge-diag {\n\ \ transform-origin: 50% 50%;\n\ \ position: absolute;\n\ \ height: 6px;\n\ \}\n\ \.edge-hor-left {\n\ \ border-bottom-left-radius: 100% 33px;\n\ \ border-left: 6px solid #578999;\n\ \}\n\ \.edge-hor-right {\n\ \ border-bottom-right-radius: 100% 33px;\n\ \ border-right: 6px solid #578999;\n\ \}\n\ \.edge-hor-no-curve {\n\ \ border-right: 6px solid #578999;\n\ \}\n\ \.edge-end {\n\ \ position: absolute;\n\ \ height: 27px;\n\ \ width: 16px;\n\ \ border-top: 6px solid #578999;\n\ \ opacity: 0.4;\n\ \ margin-left: 15px;\n\ \ margin-top: 8px;\n\ \ z-index: -1;\n\ \}\n\ \.edge-end-left {\n\ \ border-top-right-radius: 100% 33px;\n\ \ border-right: 6px solid #578999;\n\ \}\n\ \.edge-end-no-curve {\n\ \ border-right: 6px solid #578999;\n\ \ margin-top: 14px;\n\ \ height: 21px;\n\ \}\n\ \.edge-end-right {\n\ \ border-top-left-radius: 100% 33px;\n\ \ border-left: 6px solid #578999;\n\ \}\n\ \.edge-guard {\n\ \ border-color: #69B5A7;\n\ \}\n\ \.edge-unify {\n\ \ border-color: #8CBF7A;\n\ \}\n\ \.edge-alt {\n\ \ height: 1px;\n\ \ background-color: #aaa;\n\ \ position: absolute;\n\ \ margin-top: 19px;\n\ \ z-index: -1;\n\ \ padding-right: 22px;\n\ \}\n\ \"