On this page:
16.1 Visualizing ASTs
16.2 Using dot
16.3 Using graphviz programmatically
7.4

16 Using Graphviz/dot to visualize our AST

    16.1 Visualizing ASTs

    16.2 Using dot

    16.3 Using graphviz programmatically

16.1 Visualizing ASTs

Abstract Syntax Trees (ASTs) are a useful abstraction when dealing with programming languages as an object for analysis or manipulation (e.g. compilation). At the same time, these structures can quickly become too large reason about just by looking at it. For example, in Knock, our AST for (if (zero? x) (add1 (add1 x)) (sub1 x)) looks like the following:

(if-e
 (prim-e 'zero? (list (var-e 'x)))
 (prim-e 'add1 (list (prim-e 'add1 (list (int-e 1)))))
 (prim-e 'sub1 (list (var-e 'x))))

This has all the information necessary for manipulating our program (and more), it’s a bit unwieldy to look at. Particularly when debugging, it can be useful to see the overal shape of the AST. This is particularly true when speaking about program transformations.

Take, for example, the program transformation from the first Midterm. Applying that transformation (updated for Knock) to the above program, results in the following AST:

(if-e
 (prim-e 'zero? (list (var-e 'x)))
 (let-e
  (list (binding 'g387 (prim-e 'add1 (list (int-e 1)))))
  (prim-e 'add1 (list (var-e 'g387)))
 (prim-e 'sub1 (list (var-e 'x)))))

Was the program transformation done correctly? If you study the AST carefully, you can determine that it was. However, it would be easier if we could, at a glance, answer the question “Are primitive operations only applied to simple (i.e. not nested) expressions?”

Using diagrams makes answering this question marginally easier:

Before transformation:

After transformation:

The diagram above helps us visualize the transformed AST, but we still have to study the diagram carefully to know which nodes correspond to primitive operations (which are the subject of the transformation). This can be remedied easily, by coloring these nodes differently:

Before transformation:

After transformation:

These diagrams were made using dot a tool provided by the Graphviz, which is a set of software components for visualizing graphs.

16.2 Using dot

Graphviz has many components, but we will focus on dot, which is the tool for laying out directed graphs. The full manual for dot can be found on the graphviz website: The Dot Guide.

Instructions for downloading Graphviz (and therefore dot) can be found on their website as well: Download Graphviz

The syntax for dot files is fairly straightforward, you first declare the type of graph, and give it a name. For our purposes the type will always be digraph (i.e. directed-graph), and the name can be whatever you choose (though it will likely not matter much). For example:

digraph CMSC430 {

  ...

}

The ellipses are where you describe the graph you’d like to visualize. The designers of Graphviz provide a grammar describing the language accepted by their tools (I wish all system designers provided a grammar!). This can be found on the Graphviz website: The DOT language.

Most of the time you will not need to consult the grammar, as most of the simple rules are straightforward for those that have programmed in C or Java.

In short, the description of a graph is a list of statements, statements can take many forms, but for this course (and most likely for any uses beyond this course), you can basically just use the following three types of statements:

Node statements are just an ASCII string (representing a Node ID) and an optional list of attributes for that node. For example:

digraph CMSC430 {

  lexer;

  parser [shape=box];

  code_gen [color=red];

}

Using the dot tool on a file with the above as its contents produces the following diagram:

Edge statements connect nodes in our graph, for example:

digraph CMSC430 {

  lexer -> parser -> code_gen;

  parser [shape=box];

  code_gen [color=red];

}

This produces the following diagram:

You may wonder if the order matters here. While the horizontal order matters when specifying the edges in an edge statement, the vertical order does not matter in this case. The following produces the same diagram:

digraph CMSC430 {

  parser [shape=box];

  code_gen [color=red];

  lexer -> parser -> code_gen;

}

Notice that lexer does not have its own ‘declaration’ this is because it is unnecessary unless you want to attach attributes to a node (as we do with parser and code_gen).

Edge statements also support an optional list of attributes, the following produces a similar diagram except that both edges are shaded “deeppink2” (for the full list of supported colors, see the official documentation).

digraph CMSC430 {

  lexer -> parser -> code_gen [color=deeppink2];

  parser [shape=box];

  code_gen [color=red];

}

Attribute nodes describe a set of attributes that apply to all subsequent statements (which means that vertical order does matter here!). Unless overridden by a specific attribute, all statements following an attribute statement will ‘default’ to the attributes specified in the statement.

Here we added three attribute statements. Take a minute to study the example below and see how each attribute statement affects the output.

digraph CMSC430 {

  edge [color=blue];

  lexer -> parser

  edge [color=deeppink2];

  node [shape=triangle];

  parser -> optimizer;

  parser [shape=box];

  code_gen [color=red];

  optimizer -> code_gen;

}

16.3 Using graphviz programmatically

What we’ve done is write a small Racket library that abstracts away some of the details of making dot diagrams so that we can automatically generate digrams from our AST. One such detail is that we have to generate unique node IDs for each node in our AST (we do this using gensym), but then add attributes that label our nodes with the relevant information (e.g. that it’s an if node).

Here is an example of a dot description make using our library on the program (if (zero? x) 1 2):

digraph prog {

  g850 [ label=" x " ];

  g849 [ color=red,label=" (zero? ...) " ];

  g849 -> g850 ;

  g851 [ label=" 1 " ];

  g852 [ label=" 2 " ];

  g848 [ label=" if " ];

  g848 -> g849 ;

  g848 -> g851 ;

  g848 -> g852 ;

}

Not super nice to read, but we had a program write it for us!

The complete library (three files):

knock/dot.rkt

  #lang racket
  (provide (all-defined-out))
   
  (require "pretty-printer.rkt")
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; top-level graph
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; A graph is its name along with its list of contents
  ; graph : String [Content] -> Graph
  (struct graph (name conts) #:transparent)
   
  ; A subgraph is its name along with its list of contents
  ; subgraph : String [Content] -> Graph
  (struct subgraph (name conts) #:transparent)
   
  ; type Content =
  ;   | Node
  ;   | Edge
  ;   | ToMany    ; Edge from one shared parent to many targets
  ;   | Attribute
  ;   | Subgraph
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Graph internal structures
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ;;;;; Nodes
   
  ; type Node =
  ;   | String                <- Just a node
  ;   | Symbol                <- Just a node
  ;   | (String, [Property])  <- A node and some property
  ;   | SubG                  <- Subgraph (i.e. a set of nodes)
   
  ; An edge defines the origin node and the target node by name
  ; edge : Node Node [Property] -> Content
  (struct edge (orig targ props) #:transparent)
   
  ; When one origin has many targets
  ; edge : Node [Node] [Property] -> Content
  (struct to-many (orig targs props) #:transparent)
   
  ; An attribute described whether it applies to the edges or
  ; nodes and also has a list of properties
  ; attr : Symbol [Property] -> Content
  (struct attr (type props) #:transparent)
   
  ; A property is a parameter and its value
  (struct prop (par val) #:transparent)
   
  ; Pair
  (struct pair (a b) #:transparent)
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Constructors
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; Make an edge between two nodes with no properties
  ; e : Node Node -> Content
  (define (e n1 n2)
    (edge n1 n2 '()))
   
  ; Make edges between a list of nodes
  ; es [Node] -> [Content]
  (define (es ns)
    (match ns
      [(cons n1 (cons n2 '())) (cons (edge n1 n2 '()) '())]
      [(cons n1 (cons n2 ns))  (cons (edge n1 n2 '()) (es (cons n2 ns)))]
      [_  (error "There must be at least 2 nodes given to es")]))
   
  (define (tm n1 ns)
    (to-many n1 ns '()))
   
  ; Set a node to a specific shape
  ; There are many, but we'll start with:
  ;  * box
  ;  * oval
  ;  * circle
  ;  * diamond
  ;  * rect
  ;
  ; shape : Symbol Node -> Node
  (define (shape sh node)
    (match node
      [(? string? n)  (pair n (list (prop 'shape sh)))]
      [(? symbol? n)  (pair (symbol->string n) (list (prop 'shape sh)))]
      [(pair n props) (pair n (cons (prop 'shape sh) props))]))
   
  ; Attach meta-data to content, the parameter must be passed
  ; as a label
  ; attach : Symbol Symbol Content -> Content
  (define (attach param st cont)
    (match cont
     ; Nodes
      [(? string? n)  (pair n (list (prop param st)))]
      [(? symbol? n)  (pair (symbol->string n) (list (prop param st)))]
      [(pair n props) (pair n (cons (prop param st) props))]
     ; Edges
      [(edge o t props)    (edge o t (cons (prop param st) props))]
      [(to-many o t props) (to-many o t (cons (prop param st) props))]
     ; Attribute
      [(attr t props) (attr t (cons (prop param st) props))]))
   
  ; Color some Content
  ; Known colors:
  ;  * red
  ;  * blue
  ;  * black
  ;  * darkgreen
  ;  * brown
  ;  * deeppink2
  (define (color val cont)
    (attach 'color (string->symbol val) cont))
   
  ; Set the style of some contents
  ; There are many, but we'll start with:
  ;  * dashed
  ;  * dotted
  ;  * bold
  ;
  ; style : Symbol Content -> Content
  (define (style st cont)
    (attach 'style st cont))
   
  ; Attach a label to Content
  (define (label lab cont)
    (attach 'label (string->symbol lab) cont))
   
  ; Attach a label to Content
  (define (label-str lab str)
    (attach 'label (string->symbol lab) (string->symbol str)))
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Getters
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  (define (get-direct-name cont)
    (match cont
     ; Nodes
      [(? string? n)    (list n)]
      [(? symbol? n)    (list n)]
      [(pair n props)   (list n)]
     ; Edges
      [(edge o t props) '()]
      [(to-many o ts props) '()]
     ; Subgraph
      [(subgraph name cs) '()]
     ; Attribute
      [(attr t props)   '()]))
   
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Pretty-Printing
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; ppr-graph : Graph -> Seq
  (define (ppr-graph g)
    (match g
      [(graph n cs)
        (let ((header (+++ (str "digraph") (str n)))
              (body   (vert (map ppr-cont cs))))
             (+++ header (curl-ind body)))]))
   
  ; ppr-cont : Content -> Seq
  (define (ppr-cont c)
    (match c
     ; Nodes
      [(? string? n)    (str n)]
      [(? symbol? n)    (sym n)]
      [(pair n props)   (end-semi (+++ (str n) (ppr-props props)))]
     ; Edges
      [(edge o t props) (end-semi (ppr-to-target o t props))]
      [(to-many o ts props)
        (vert (map (lambda (x) (end-semi (ppr-to-target o x props))) ts))]
     ; Subgraph
      [(subgraph name cs)
        (let ((header (+++ (str "subgraph") (str name)))
              (body   (vert (map (compose end-semi ppr-cont) cs))))
             (+++ header (curl-ind body)))]
     ; Attribute
      [(attr t props)   (end-semi (+++ (str t) (ppr-props props)))]))
   
  ; Helper function for above
  (define (ppr-to-target o t props)
    (+++ (+-> (ppr-cont o) (ppr-cont t))
              (ppr-props props)))
   
  ; ppr-props : [Property] -> Seq
  (define (ppr-props ps)
    (match ps
      ['() (nil)]
      [ps (sqr (comma-sep (map ppr-prop ps)))]))
   
  ; ppr-prop : Property -> Seq
  (define (ppr-prop p)
    (match p
      [(prop 'label val) (+= (sym 'label) (qt (sym val)))]
      [(prop par val) (+= (sym par) (sym val))]))
   

knock/render-ast.rkt

  #lang racket
  (provide (all-defined-out))
   
  (require "ast.rkt")
  (require "dot.rkt")
   
  (define (render-prog p fn)
    (match p
      [(prog '() e)
        (match (render-expr e)
          [(cons _ res) (graph fn res)])]
      [(prog ds e)
        (match (render-expr e)
          [(cons _ res) (graph fn res)])]))
   
  (define (render-fun f)
    (match f
      [(fundef name args body)
        (match (render-expr e)
          [(cons _ res) (subgraph name res)])]))
   
  ; render-expr : Expr -> (ID, [Content])
  (define (render-expr e)
    (let ((i (gensym)))
    (match e
      [(nil-e)       (cons i (list (label "nil" i)))]
      [(int-e v)     (cons i (list (label (~a v) i)))]
      [(bool-e b)    (cons i (list (label (~a b) i)))]
      [(var-e v)     (cons i (list (label (symbol->string v) i)))]
      [(char-e c)    (cons i (list (label (~a c) i)))]
      [(fun-e f)     (cons i (list (label (~a "fun " f) i)))]
      [(call-e f es)
        (cons i (let ((l (label (~a "(call " (fname f) " ...)") i)))
                     (render-subs i l es)))]
      [(app-e f es)
        (cons i (let ((l (label (~a "(" f " ...)") i)))
                     (render-subs i l es)))]
      [(prim-e p es)
        (cons i (let* ((l (label (~a "(" (symbol->string p) " ...)") i))
                       (l2 (color "red" l)))
                      (render-subs i l2 es)))]
      [(if-e e t f)
        (cons i (let ((l (label "if" i)))
                     (render-subs i l (list e t f))))]
      [(let-e bs b)  (cons i (render-let i "let" bs b))])))
   
  ; On big programs this will be bad (it's n^2, I think)
  (define (render-subs pid parent es)
      (let* ((subps (map render-expr es))
             (subs  (append* (map cdr subps)))
             (ids (map car subps)))
            `(,@subs
              ,parent
              ,(tm pid
                   ids))))
   
  (define (render-let pid str bs body)
    (let* ((bind (render-binding bs))
           (body (render-expr body))
           (parent (label str pid)))
    `(,@(cdr bind)
      ,@(cdr body)
      ,parent
      ,(tm pid (list (car body) (car bind))))))
   
  (define (render-binding bs)
    (match bs
      [(cons (binding v body) '())
        (let* ((i (gensym))
               (vn (label (symbol->string v) i))
               (bn (render-expr body))
               (cid (car bn)))
              (cons i `(,@(cdr bn) ,vn ,(color "deeppink2" (e i cid)))))]))
   
  (define (fname f)
    (match f
      [(fun-e f) f]
      [_         f]))
   

knock/pretty-printer.rkt

  #lang racket
  (provide (all-defined-out))
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Principal data structure for describing pretty-printed things
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; type Seq
  ; | Str String
  ; | Nil
  ; | Newline
  ; | Indent Seq
  ; | Append Seq Seq
   
  (struct str    (s)     #:transparent)
  (struct nil    ()      #:transparent)
  (struct nl     ()      #:transparent)
  (struct indent (s)     #:transparent)
  (struct ++     (s1 s2) #:transparent)
   
  ;(list (cons (list (++ (++ (str "main") 
  ;                      (++ (str " ") (++ (str "->")
  ;                      (++ (str " ") (str "print")))))
  ;                      (++ (str " ") (nil)))) 
  ;             2)
  ;      (cons (++ (nl) (str "}")) 0))
  ; Efficiently converting that data structure into a string, avoiding the
  ; n^2 append when dealing with left-nested appends
  (define (flatten-seq col ss)
    (match ss
      ['() ""]
      [(cons (cons (indent s) i) rest) (flatten-seq col `((,s . ,col) ,@rest))]
      [(cons (cons (nil) i)      rest) (flatten-seq col rest)]
      [(cons (cons (str s) i)    rest) (string-append s
                                         (flatten-seq (+ (string-length s) i) rest))]
      [(cons (cons (++ s1 s2) i) rest) (flatten-seq col `(,(cons s1 i)
                                                      ,(cons s2 i)
                                                      ,@rest))]
      [(cons (cons (nl) i)       rest) (string-append
                                         "\n"
                                         (make-string i #\ )
                                         (flatten-seq i rest))]))
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Pretty-printing API
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; Top-level pretty-print, 
  (define (seq->string seq)
    (flatten-seq 0 (list (cons seq 0))))
   
  ;;;;; Appending Things
   
  ; Append two things with a space in between
  (define (+++ s1 s2)
    (++ s1 (++ ss s2)))
   
  ; Append two things with an arrow in between
  (define (+-> s1 s2)
    (+++ s1 (+++ (str "->") s2)))
   
  ; Append two things with an equals in between (no space)
  (define (+= s1 s2)
    (++ s1 (++ (str "=") s2)))
   
  ; Append two things with an equals in between (space)
  (define (+=+ s1 s2)
    (+++ s1 (+++ (str "=") s2)))
   
  ;;;;; List of sequences
  ;;;;;   * no separation
  ;;;;;   * space separated
  ;;;;;   * Comma separated
  ;;;;;   * semi-colon separated
   
  (define (lst seqs sep)
    (match seqs
      ['() (nil)]
      [(cons s '()) s]
      [(cons s ss)  (++ s (++ sep (lst ss sep)))]))
   
  (define (no-sep seqs)
    (lst seqs (nil)))
   
  (define (sp-sep seqs)
    (lst seqs ss))
   
  (define (comma-sep seqs)
    (lst seqs com))
   
  (define (semi-sep seqs)
    (lst seqs sem))
   
  (define (vert seqs)
    (lst seqs (nl)))
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Enclosing things
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; Enclose a sequence with the given open and close sequences
  (define (enc open close seq)
    (+++ open (+++ seq close)))
   
  ; Enclose the given sequence with curly braces
  (define (curly seq)
    (enc (str "{") (str "}") seq))
   
  ; Enclose the given sequence with square braces
  (define (sqr seq)
    (enc (str "[") (str "]") seq))
   
  ; Enclose the given sequence with parentheses
  (define (par seq)
    (enc (str "(") (str ")") seq))
   
  ; Enclose the given sequence with double-quotes
  (define (qt seq)
    (enc (str "\"") (str "\"") seq))
   
  ;; Same as above but with an indented sequence
   
  ; This is ugly: TODO: Think of a better way
  (define (enc-ind open close seq)
    (++ open (++ (nl)
                 (++ (space 2)
                     (++ (indent seq)
                          (++ (nl) close))))))
   
  ; Enclose and indent with curly braces
  (define (curl-ind seq)
    (enc-ind (str "{") (str "}") seq))
   
  ; Enclose and indent with square braces
  (define (sqr-ind seq)
    (enc-ind (str "[") (str "]") seq))
   
  ; Enclose and indent with parantheses
  (define (par-ind seq)
    (enc-ind (str "(") (str ")") seq))
   
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; End line with various things
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  (define (end-semi seq)
    (++ seq (++ sem (nl))))
   
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;; Miscelanious Characters or Symbols
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
  ; When you have a symbol
  (define (sym s)
    (str (symbol->string s)))
   
  ; A set number of spaces
  (define (space i)
    (str (make-string i #\ )))
   
  ; A single space
  (define ss (str " "))  ; single space
   
  ;;; Comma, semi-colons, and colons
  (define com (str ","))
  (define sem (str ";"))
  (define col (str ":"))