{- | Generates a visual representation of a control flow graph, by overlaying a pretty-printed syntax output with a graphviz-generated graph. This code uses the command line tools \"neato\", \"pdf2ps\" and \"pdftk\". -} {-# LANGUAGE TypeOperators #-} module CFGraph where import Graphics.PDF import Language.Dot import Data.Map (keys,(!)) import Text.Printf import System.Process import System.Directory import System.IO import CPSScheme import CPSPrint import Common -- | The font that is used to generate the code listings. font :: PDFFont font = PDFFont Courier 11 -- | Assuming a mono-spaced font, this is the width of a character. theCharWidth :: PDFFloat theCharWidth = charWidth font 'M' -- | The height of a character. theCharHeight :: PDFFloat theCharHeight = getHeight font -- | Creates a PDF file containing the given text, without any padding or -- borders, using the font specified by 'font' renderCodeToFile :: FilePath -> String -> IO () renderCodeToFile fn code = do runPdf fn standardDocInfo pageRect $ do page <- addPage Nothing drawWithPage page $ sequence $ zipWith (\ln line -> drawText $ text font 0 (fromIntegral ln * theCharHeight) (toPDFString line)) [lineNumber-1,lineNumber-2..0] ls where ls = lines (removeLambdas code) lineLength = maximum (map length ls) lineNumber = length ls pageWidth = ceiling (fromIntegral lineLength * theCharWidth) pageHeight = ceiling (fromIntegral lineNumber * theCharHeight) pageRect = PDFRect 0 0 pageWidth pageHeight -- | Creates a 'Graph' createDotFromGraph :: Integer -- ^ number of lines -> Integer -- ^ number of columns -> [Label :× Label] -- ^ the list of edges to draw -> (Label :⇀ (Integer, Integer)) -- ^ the position of the -- nodes, in characters -> Graph createDotFromGraph ls cs edges coords = Graph UnstrictGraph DirectedGraph Nothing (settings ++ nodes ++ edges') where settings = [ AttributeStatement GraphAttributeStatement [ AttributeSetValue (NameId "bb") (StringId (printf "0,0,%.4f,%.4f" (width) (height))) , AttributeSetValue (NameId "pad") (StringId "0") , AttributeSetValue (NameId "splines") (StringId "true") ] , AttributeStatement NodeAttributeStatement [ AttributeSetValue (NameId "shape") (StringId "point") , AttributeSetValue (NameId "height") (StringId "0.03") ] , AttributeStatement EdgeAttributeStatement [ AttributeSetValue (NameId "penwidth") (StringId "0.4") , AttributeSetValue (NameId "arrowsize") (StringId "0.2") , AttributeSetValue (NameId "color") (StringId "#0000FF80") ] ] nodes = map (\l -> let (x,y) = charToPt (coords ! l) in NodeStatement (labelToId l) [ AttributeSetValue (NameId "pos") (StringId (printf "%.4f,%.4f" x y)) ] ) (keys coords) edges' = map (\(l1,l2) -> EdgeStatement [ ENodeId NoEdge (labelToId l1), ENodeId DirectedEdge (labelToId l2) ] []) edges labelToId (Label i) = NodeId (IntegerId i) Nothing charToPt (r,c) = ( (fromIntegral c-0.5) * theCharWidth, (fromIntegral (ls-r)+0.2) * theCharHeight ) width = fromIntegral $ ceiling (fromIntegral cs * theCharWidth) :: Double height = fromIntegral $ ceiling (fromIntegral ls * theCharHeight) :: Double -- | Creates a 'Graph' given a program and a function generating the required -- graph createDotFromCode :: (Prog -> [Label :× Label]) -> Prog -> Graph createDotFromCode eval prog = createDotFromGraph lineNumber lineLength edges coords where edges = eval prog (coords, code) = labelPositions '*' $ renderProg True prog ls = lines (removeLambdas code) lineLength = fromIntegral $ maximum (map length ls) lineNumber = fromIntegral $ length ls -- | The main function of this module. Writes out a PDF file containing both -- code and control flow graph createCodeWithGraph :: (Prog -> [Label :× Label]) -- ^ Generating a graph from a program -> FilePath -- ^ Wanted filename -> Prog -- ^ Program to draw -> IO () createCodeWithGraph eval filename prog = do renderCodeToFile codeFileName code let neato = (proc "neato" ["-n","-s","-Tps2"]) { std_in = CreatePipe, std_out = CreatePipe } (Just input, Just pipe, _ ,_) <- createProcess neato hPutStr input graph hClose input let ps2pdf = (proc "ps2pdf" ["-", graphFileName]) { std_in = UseHandle pipe, std_out = CreatePipe } (_ , _ , _, ph) <- createProcess ps2pdf waitForProcess ph let pdftk = proc "pdftk" [graphFileName, "background", codeFileName, "output", filename] (_ , _ , _, ph) <- createProcess pdftk waitForProcess ph removeFile graphFileName removeFile codeFileName where code = removeLambdas $ snd $ labelPositions ' ' $ renderProg True prog codeFileName = filename ++ ".tmp1.pdf" graphFileName = filename ++ ".tmp2.pdf" graph = renderDot $ createDotFromCode eval prog