canontra-0.1.0.0: src/Canontra/Export/Graph.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{- |
Module : Canontra.Export.Graph
Description : Graphviz DOT visualization generators for Call Graphs, CFGs, and DFGs.
Exports semantic graphs into standard Graphviz DOT representations for visual inspection,
compiler tooling, and documentation. Note: Mermaid export is excluded per design mandate.
-}
module Canontra.Export.Graph
( exportCallGraphDOT
, exportCFGDOT
, exportDFGDOT
, escapeDOT
, sanitizeDOTId
) where
import Data.List (nub)
import Data.Text (Text)
import qualified Data.Text as T
import Canontra.Analysis.CallGraph
( CallEdge (..)
, CallGraph (..)
, CalleeTarget (..)
, CallerNode (..)
)
import Canontra.Analysis.CFG
( BasicBlock (..)
, BranchCondition (..)
, CFGEdge (..)
, ControlFlowGraph (..)
)
import Canontra.Analysis.DFG
( DFGEdge (..)
, DFGNode (..)
, DataFlowGraph (..)
, DefUseKind (..)
)
-- | Escape characters for valid Graphviz DOT string literals.
escapeDOT :: Text -> Text
escapeDOT = T.concatMap escapeChar
where
escapeChar '"' = "\\\""
escapeChar '\\' = "\\\\"
escapeChar '\n' = "\\n"
escapeChar '\r' = ""
escapeChar c = T.singleton c
-- | Sanitize a Text string into a valid DOT node identifier.
sanitizeDOTId :: Text -> Text
sanitizeDOTId = T.map cleanChar
where
cleanChar c
| c >= 'a' && c <= 'z' = c
| c >= 'A' && c <= 'Z' = c
| c >= '0' && c <= '9' = c
| otherwise = '_'
-- ============================================================================
-- Call Graph DOT Export
-- ============================================================================
-- | Export a CallGraph into a standard Graphviz DOT format string.
exportCallGraphDOT :: CallGraph -> Text
exportCallGraphDOT cg =
T.unlines $
[ "digraph CallGraph {"
, " rankdir=LR;"
, " node [shape=box, fontname=\"Helvetica\", style=\"rounded,filled\", fillcolor=\"#f0f4f8\", color=\"#4a5568\"];"
, " edge [fontname=\"Helvetica\", color=\"#718096\"];"
, ""
]
++ map declareNode allNodes
++ [ "" ]
++ map declareEdge (cgEdges cg)
++ [ "}" ]
where
allCallers = cgNodes cg
allCallees = nub [edgeCallee e | e <- cgEdges cg]
allNodes = map Left allCallers ++ [Right c | c <- allCallees, not (calleeIsCaller c)]
calleeIsCaller (TargetLocal n) = CallFunction n `elem` allCallers
calleeIsCaller _ = False
nodeId (Left caller) = "caller_" <> sanitizeCaller caller
nodeId (Right callee) = "callee_" <> sanitizeCallee callee
sanitizeCaller CallTopLevel = "toplevel"
sanitizeCaller (CallFunction fn) = "fn_" <> sanitizeDOTId fn
sanitizeCaller (CallMethod cls m) = "method_" <> sanitizeDOTId cls <> "_" <> sanitizeDOTId m
sanitizeCallee (TargetLocal n) = "local_" <> sanitizeDOTId n
sanitizeCallee (TargetMethod cls m) = "meth_" <> sanitizeDOTId cls <> "_" <> sanitizeDOTId m
sanitizeCallee (TargetImported m s) = "imp_" <> sanitizeDOTId m <> "_" <> sanitizeDOTId s
sanitizeCallee (TargetDynamic _) = "dyn_" <> sanitizeDOTId "dynamic"
labelCaller CallTopLevel = "<top-level>"
labelCaller (CallFunction fn) = "def " <> fn
labelCaller (CallMethod cls m) = cls <> "." <> m
labelCallee (TargetLocal n) = n
labelCallee (TargetMethod cls m) = if T.null cls then m else cls <> "." <> m
labelCallee (TargetImported m s) = m <> "." <> s
labelCallee (TargetDynamic _) = "<dynamic>"
declareNode n@(Left caller) =
" \"" <> nodeId n <> "\" [label=\"" <> escapeDOT (labelCaller caller) <> "\"];"
declareNode n@(Right callee) =
" \"" <> nodeId n <> "\" [label=\"" <> escapeDOT (labelCallee callee)
<> "\", fillcolor=\"#edf2f7\", style=\"dashed,rounded,filled\"];"
declareEdge (CallEdge caller callee cnt isAsync) =
let srcId = nodeId (Left caller)
dstId = if calleeIsCaller callee
then nodeId (Left (toCaller callee))
else nodeId (Right callee)
cntLabel = if cnt > 1 then T.pack (show cnt) <> "x" else ""
asyncLabel = if isAsync then "async" else ""
edgeLabel = case (T.null cntLabel, T.null asyncLabel) of
(True, True) -> ""
(False, True) -> cntLabel
(True, False) -> asyncLabel
(False, False) -> cntLabel <> ", " <> asyncLabel
labelAttr = if T.null edgeLabel then "" else "label=\"" <> escapeDOT edgeLabel <> "\""
asyncStyle = if isAsync then "style=\"dashed\", color=\"#3182ce\"" else ""
attrs = filter (not . T.null) [labelAttr, asyncStyle]
attrStr = if null attrs then "" else " [" <> T.intercalate ", " attrs <> "]"
in " \"" <> srcId <> "\" -> \"" <> dstId <> "\"" <> attrStr <> ";"
toCaller (TargetLocal n) = CallFunction n
toCaller _ = CallTopLevel
-- ============================================================================
-- Control-Flow Graph (CFG) DOT Export
-- ============================================================================
-- | Export a list of ControlFlowGraphs into a Graphviz DOT representation.
exportCFGDOT :: [ControlFlowGraph] -> Text
exportCFGDOT cfgs =
T.unlines $
[ "digraph ControlFlowGraph {"
, " rankdir=TB;"
, " node [fontname=\"Courier\", shape=box, style=\"filled\"];"
, " edge [fontname=\"Helvetica\", color=\"#4a5568\"];"
, ""
]
++ concatMap formatSingleCFG (zip [(0 :: Int) ..] cfgs)
++ [ "}" ]
where
formatSingleCFG (idx, cfg) =
let clusterName = "cluster_cfg_" <> T.pack (show idx)
fnName = cfgFunction cfg
in [ " subgraph \"" <> clusterName <> "\" {"
, " label=\"" <> escapeDOT fnName <> "\";"
, " style=\"rounded\";"
, " color=\"#cbd5e0\";"
, " fillcolor=\"#f7fafc\";"
, ""
]
++ map (formatBlock idx) (cfgBlocks cfg)
++ map (formatEdge idx) (cfgEdges cfg)
++ [ " }"
, ""
]
blockId idx bId = "bb_" <> T.pack (show idx) <> "_" <> T.pack (show bId)
formatBlock idx bb =
let bIdent = blockId idx (bbId bb)
bLabel = "bb" <> T.pack (show (bbId bb))
stmtsCount = length (bbStatements bb)
stmtInfo = if stmtsCount == 0 then "" else "\\n(" <> T.pack (show stmtsCount) <> " stmts)"
color = if bbId bb == 0 then "#ebf8ff" else "#ffffff"
in " \"" <> bIdent <> "\" [label=\"" <> escapeDOT (bLabel <> stmtInfo)
<> "\", fillcolor=\"" <> color <> "\"];"
formatEdge idx edge =
let src = blockId idx (edgeFrom edge)
dst = blockId idx (edgeTo edge)
condText = formatCondition (edgeCondition edge)
attrStr = if T.null condText then "" else " [label=\"" <> escapeDOT condText <> "\"]"
in " \"" <> src <> "\" -> \"" <> dst <> "\"" <> attrStr <> ";"
formatCondition (CondTrue _) = "True"
formatCondition (CondFalse _) = "False"
formatCondition (CondCase _) = "Case"
formatCondition CondDefault = "Default"
formatCondition CondUnconditional = ""
formatCondition (CondException ex) = "catch " <> ex
-- ============================================================================
-- Data-Flow Graph (DFG) DOT Export
-- ============================================================================
-- | Export a list of DataFlowGraphs into a Graphviz DOT representation.
exportDFGDOT :: [DataFlowGraph] -> Text
exportDFGDOT dfgs =
T.unlines $
[ "digraph DataFlowGraph {"
, " rankdir=LR;"
, " node [fontname=\"Courier\", style=\"filled\"];"
, " edge [fontname=\"Helvetica\", color=\"#2b6cb0\"];"
, ""
]
++ concatMap formatSingleDFG (zip [(0 :: Int) ..] dfgs)
++ [ "}" ]
where
formatSingleDFG (idx, dfg) =
let clusterName = "cluster_dfg_" <> T.pack (show idx)
fnName = dfgFunction dfg
in [ " subgraph \"" <> clusterName <> "\" {"
, " label=\"" <> escapeDOT fnName <> "\";"
, " style=\"rounded\";"
, " color=\"#e2e8f0\";"
, " fillcolor=\"#faf5ff\";"
, ""
]
++ map (formatNode idx) (dfgNodes dfg)
++ map (formatEdge idx) (dfgEdges dfg)
++ [ " }"
, ""
]
nodeIdent idx nId = "dfg_" <> T.pack (show idx) <> "_" <> T.pack (show nId)
formatNode idx node =
let nId = nodeIdent idx (dfgNodeId node)
(nLabel, nShape, nFill, nBorder) = describeKind (dfgKind node)
in " \"" <> nId <> "\" [label=\"" <> escapeDOT nLabel
<> "\", shape=" <> nShape <> ", fillcolor=\"" <> nFill
<> "\", color=\"" <> nBorder <> "\"];"
describeKind (DefParam i) =
("Param " <> T.pack (show i), "ellipse", "#e6fffa", "#319795")
describeKind (DefAssignment var) =
("Def " <> var, "ellipse", "#e6fffa", "#319795")
describeKind (DefPhi ids) =
("Phi " <> T.pack (show ids), "diamond", "#faf5ff", "#805ad5")
describeKind (UseRead var) =
("Use " <> var, "box", "#ebf8ff", "#3182ce")
describeKind (UseArgument i) =
("Arg " <> T.pack (show i), "box", "#ebf8ff", "#3182ce")
describeKind UseBranchGuard =
("Guard", "hexagon", "#fffaf0", "#dd6b20")
formatEdge idx edge =
let src = nodeIdent idx (dfgSource edge)
dst = nodeIdent idx (dfgTarget edge)
var = dfgVarName edge
attrStr = if T.null var then "" else " [label=\"" <> escapeDOT var <> "\"]"
in " \"" <> src <> "\" -> \"" <> dst <> "\"" <> attrStr <> ";"