Groovy AST の勉強 その2

今回のお題:
・入力となるソースコードコンパイルする。
・メインクラスの ClassNode を取得する。
・取得した ClassNode のツリー構造を画像データにレンダリングする。

次のようなステップで処理を行う。

(1) groovy スクリプトのファイルを読み出してコンパイルし AST を取り出す。
(2) メインクラスの ClassNode を取得し、その配下の全ノードをトラバースして対応する XML ツリーを作成する。
(3) XML から Graphviz の DOT 形式に変換する。
(4) DOT 形式から Graphviz*1 の dot コマンドで PNG 画像を出力する。

いきなり (2) で DOT にすればいいじゃん!無駄じゃん!と思われるかもしれないが、AST を一度 XML にしておけば、色んな加工方法が考えられるので「あえて」XML にしている。

それぞれのステップを見ていく。

(1) groovy スクリプトのファイルを読み出してコンパイルし AST を取り出す。

ModuleNode src2ast (File infile, CompilePhase compilePhase) {
  def name = infile.name.substring(0,infile.name.indexOf(".groovy"))
  def source = infile.text
  def sourceUnit = SourceUnit.create(name, source)
  def compUnit = new CompilationUnit()
  compUnit.addSource(sourceUnit)
  compUnit.compile(compilePhase.phaseNumber)
  sourceUnit.getAST()
}

前回みつけた、ソースコードコンパイルして AST を取り出すコード。
SourceUnitの名前にはスクリプトのファイル名を使用。

(2) メインクラスの ClassNode を取得し、その配下の全ノードをトラバースして対応する XML ツリーを作成する。

String ast2xml (ModuleNode mn) {
  def sb = new StringBuffer()
  def visitor = new MyVisitor(sb)
  sb << "<ast>\n"
  sb << "<class>\n"
  visitor.visitClass(pickupMainClassNode(mn))
  sb << "</class>\n"
  sb << "</ast>\n"
  sb.toString()
}

GroovyClassVisitor と GroovyCodeVisitor を実装した MyVisitor クラスでメインクラスの ClassNode 配下をトラバースする。MyVisitor クラスはややボリュームがあるのでこのエントリの末尾に掲載しておく。

(3) XML から Graphviz の DOT 形式に変換する。

String xml2dot (GPathResult n) {
  def sb = new StringBuffer()
  sb << "digraph ast {\n"
  sb << "node [shape = record, fontname = \"Helvetica\", fontsize = 10];\n"
  node(n.class.node[0], sb)
  sb << "}\n"
  sb.toString()
}

node メソッド再帰的に XMLの Elementツリーをトラバースし、対応する DOT 形式を出力する。

(4) DOT 形式から Graphviz の dot コマンドで PNG 画像を出力する。

boolean dot2png (File dotfile, File pngfile) {
  "$dot_exe -T png -o ${pngfile} ${dotfile}".execute().waitFor()
  pngfile.exists()
}

Graphviz の dot.exe コマンドを実行。String への execute メソッドの追加はコードを簡潔に記述できて便利。

それでは、以下、2つの Groovy スクリプトの AST のツリー構造を見てみよう。

// test1.groovy
class test1 {
  public static void main (String[] args) {
    println "Hello, world!"
  }
}

test1.groovy のツリー構造:PNG SVG

// test2.groovy
println "Hello, world!"

test2.groovy のツリー構造:PNG SVG

test1 は予想通りだったが、test2 は予想を超えていて驚いた。謎の public Object run () メソッドの中に test2 のコードが挿入されている。
てっきり、main メソッドの中に挿入されるものと予想していた。しかし、run メソッドか~。

クラス定義を行わずに書いたコードは、補完されたクラスのインスタンスオブジェクトのコンテキストで実行されるわけだ。これは意識しておいた方がよさそう。

とりあえず、AST の可視化はわかったので、次は AST変換に取り組んでみることにしよう。つまり、ASTTransformation の実装である。

次回はどんな AST変換を作るか、ネタを整理してみる。


最後に今回作成したコードの全体を示す。

// ast2png.groovy
import groovy.transform.*
import org.codehaus.groovy.control.*
import org.codehaus.groovy.ast.*
import groovy.util.slurpersupport.*
import bunji.MyVisitor

def debug=false
def compilePhase = CompilePhase.CONVERSION
println "${this.class}: compile phase = ${compilePhase}"

@Field def dot_exe="C:\\opt\\graphviz\\bin\\dot.exe"
for (def x in [dot_exe]) {
  def f = new File(x)
  if (! f.exists()) {
    println "${this.class}: ${f.path} is not found!"
    return
  }
}

ModuleNode src2ast (File infile, CompilePhase compilePhase) {
  def name = infile.name.substring(0,infile.name.indexOf(".groovy"))
  def source = infile.text
  def sourceUnit = SourceUnit.create(name, source)
  def compUnit = new CompilationUnit()
  compUnit.addSource(sourceUnit)
  compUnit.compile(compilePhase.phaseNumber)
  sourceUnit.getAST()
}

ClassNode pickupMainClassNode (ModuleNode mn) {
  def mainClassName = mn.mainClassName
  if (mn.classes != null) {
    for (ClassNode cn in mn.classes) {
      if (cn.name == mainClassName) {
        return cn
      }
    }
  }
  return null
}

String ast2xml (ModuleNode mn) {
  def sb = new StringBuffer()
  def visitor = new MyVisitor(sb)
  sb << "<ast>\n"
  sb << "<class>\n"
  visitor.visitClass(pickupMainClassNode(mn))
  sb << "</class>\n"
  sb << "</ast>\n"
  sb.toString()
}

@Field
int ctr=0

String escape (String x) {
  x.replaceAll(~/&/, '&amp;')
   .replaceAll(~/</, '&lt;')
   .replaceAll(~/>/, '&gt;')
}

int node(GPathResult n, StringBuffer sb) {
  if (n.name() == "node") {
    def t = []
    def params = [:]
    n.param?.each { m ->
      def fc = m.children()[0]
      if (fc.name() == "value") {
        if (fc.@annotation != "") {
          t << "${m.@name}=\\\"${escape(fc.text())}\\\" (${fc.@annotation})"
        } else {
          t << "${m.@name}=\\\"${escape(fc.text())}\\\""
        }
      } else {
        params[m.@name] = node(fc, sb)
      }
    }

    int c = ctr++
    sb << "N$c [label = \"{(N$c)${n.@name}"

    if (t.size()>0) {
      sb << "|{"
      sb << t.join("|")
      sb << "}"
    }

    if (params.size()>0) {
      sb << "|{"
      sb << params.collect {k,v ->
          "<$k> $k"
      }.join("|")
      sb << "}}\"];\n"
      params.each {k,v ->
        sb << "N$c:<$k> -> N$v;\n"
      }
    } else {
      sb << "}\"];\n"
    }

    return c
  }

  if (n.name() == "value") {
    int c = ctr++
    sb << "N$c [label = \"(N$c)${n.text()}\"];\n"
    return c
  }

  if (n.name () == "array") {
    def a=[]
    n.children().each { m ->
      a << node(m, sb)
    }
    int c = ctr++
    sb << "N$c [label = \"{array(${n.@size})"
    if (n.@size != "0") {
      sb << "|{"
      int i=0
      sb << a.collect { m ->
        "<$i> ${i++}"
      }.join("|")
      sb << "}"
    }
    sb << "}\"];\n"
    a.eachWithIndex { m,i ->
      sb << "N$c:<$i> -> N$m;\n"
    }
    return c
  }

  if (n.name () == "param") {
    return node(n.children()[0], sb)
  }

  int c = ctr++
  sb << "N$c [label = \"(N$c)${n.name()}\"];\n"
  return c
}

String xml2dot (GPathResult n) {
  def sb = new StringBuffer()
  sb << "digraph ast {\n"
  sb << "node [shape = record, fontname = \"Helvetica\", fontsize = 10];\n"
  node(n.class.node[0], sb)
  sb << "}\n"
  sb.toString()
}

boolean dot2png (File dotfile, File pngfile) {
  "$dot_exe -T png -o ${pngfile} ${dotfile}".execute().waitFor()
  pngfile.exists()
}

def infile = new File(args[0])

def ast = src2ast (infile, compilePhase)
def xml = ast2xml(ast)

if (debug) {
  def xmlfile = new File(infile.path + ".xml")
  xmlfile.text = xml
  println "${this.class}: internal xml is saved in ${xmlfile.path}."
}

def n = new XmlSlurper().parseText(xml)
def dot = xml2dot(n)

def dotfile = new File(infile.path + ".dot")
dotfile.text = dot
if (debug) {
  println "${this.class}: internal dot is saved in ${dotfile.path}."
}

def pngfile = new File(infile.path + ".png")
if (pngfile.exists()) {
  if (pngfile.delete()) {
    println "${this.class}: old png is deleted."
  }
}
println "Rendering ${dotfile.name} to ${pngfile.name} ..."
if (dot2png(dotfile, pngfile)) {
  println "${this.class}: png is saved in ${pngfile.path}"
} else {
  println "${this.class}: failed."
}

bunji.MyVisitor は実装するメソッドが多くなかなか骨が折れたが、結果としてノード系とコード系すべてのクラスの JavaDoc に眼を通すことになり、勉強になった。

package bunji
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.ast.stmt.*
import org.codehaus.groovy.classgen.BytecodeExpression
import org.codehaus.groovy.syntax.Token
import org.codehaus.groovy.syntax.Types

public class MyVisitor implements GroovyClassVisitor,GroovyCodeVisitor
{

  static def rev_Types = [:]

  private StringBuffer sb;

  String escape (x) {
    (x == null)?x:
     x.toString().replaceAll(~/&/, '&amp;')
     .replaceAll(~/</, '&lt;')
     .replaceAll(~/>/, '&gt;')
  }

  public MyVisitor(StringBuffer sb) {

    this.sb = sb
    for (field in Types.declaredFields) {
      def k = field.name
      def v = Types[field.name]
      if (v instanceof Integer) {
        rev_Types[v]=k
      }
    }
  }

  String modifier2str (int modifier) {
    def m = []
    if (modifier & 1) {
      m << "public"
    }
    if (modifier & 2) {
      m << "private"
    }
    if (modifier & 4) {
      m << "protected"
    }
    if (modifier & 8) {
      m << "static"
    }
    if (modifier & 16) {
      m << "final"
    }
    if (modifier & 32) {
      m << "synchronized"
    }
    if (modifier & 64) {
      m << "volatile"
    }
    if (modifier & 128) {
      m << "transient"
    }
    if (modifier & 256) {
      m << "native"
    }
    if (modifier & 1024) {
      m << "abstract"
    }
    return m.join(",")
  }

  void nb(n) { // Node Begin
    sb << "<node name=\"$n\">\n"
  }
  void ne() { // Node End
    sb << "</node>"
  }
  void p(n) { // Param begin/end
    sb << "<param name=\"$n\"/>\n"
  }
  void pb(n) { // Param Begin
    sb << "<param name=\"$n\">\n"
  }
  void pe() { // Param End
    sb << "</param>\n"
  }
  void p(n,t) { // Param begin/end with value
    sb << "<param name=\"$n\">\n"
    sb << "<value>${escape(t)}</value>\n"
    sb << "</param>\n"
  }
  void p(n,t,type) { // Param begin/end with value
    sb << "<param name=\"$n\">\n"
    sb << "<value type=\"$type\">${escape(t)}</value>\n"
    sb << "</param>\n"
  }
  void p(n,t,type,annotation) { // Param begin/end with value
    sb << "<param name=\"$n\">\n"
    sb << "<value type=\"$type\" annotation=\"$annotation\">${escape(t)}</value>\n"
    sb << "</param>\n"
  }
  void pbl(n,s) { // Param Begin with Array Begin
    sb << "<param name=\"$n\">\n<array size=\"${s}\">\n"
  }
  void pel() { // Param End with Array End
    sb << "</array>\n</param>\n"
  }
  void pl(n) { // Param begin/end with Array begin/end
    sb << "<param name=\"$n\"><array size=\"0\"/></param>\n"
  }
  void ab(s) { // Array Begin
    sb << "<array size=\"${s}\">\n"
  }
  void ae() { // Array End
    sb << "</array>\n"
  }
  void a() {
    sb << "<array size=\"0\"/>\n"
  }
  String trimType(x) {
    if (x!=null && x.toString().startsWith("class ")) {
      x.toString().substring(6)
    } else {
      x
    }
  }

  void visitToken(Token t) {
    def op = escape(t.text)
    nb("Token")
    p("type", t.type, "int", rev_Types[t.type])
    p("text", op)
    ne()
  }

  void visitParameter(Parameter pa) {
    nb("Parameter")
    p("name", pa.name)
    p("type", trimType(pa.type))
    ne()
  }

  void visitProperty(PropertyNode n) {
    nb("PropertyNode")
    p("name", n.name)
    pb "initialExpression"
    n.initialExpression?.visit(this)
    pe()
    if (n.field != null) {
      pb "field"
      visitField n.field
//      n.field.visit(this)
      pe()
    }
    p("modifiers", n.modifiers, "int", modifier2str(n.modifiers))
    pb "getterBlock"
    n.getterBlock?.visit(this)
    pe()
    pb "setterBlock"
    n.setterBlock?.visit(this)
    pe()
    ne()
  }

  void visitField(FieldNode n) {
    nb("FieldNode")
    p("name", n.name)
    p("type", n.type.name)
    p("owner", n.owner.name)
    pb "initialExpression"
    n.initialExpression?.visit(this)
    pe()
    p("modifiers", n.modifiers, "int", modifier2str(n.modifiers))
    ne()
  }

  void visitConstructor(ConstructorNode n) {
    nb "ConstructorNode"
    p("modifiers", n.modifiers, "int", modifier2str(n.modifiers))
    if (n.parameters?.size() > 0) {
      pbl("parameters",n.parameters?.size())
      n.parameters.each {
        visitParameter(it)
      }
      pel()
    } else {
      pl("parameters")
    }
    if (n.exceptions?.size() > 0) {
      pbl("exceptions",n.exceptions?.size())
      n.exceptions.each {
        it.visit(this)
      }
      pel()
    } else {
      pl("exceptions")
    }
    pb("code")
    n.code.visit(this)
    pe()
    ne()
  }

  void visitClass(ClassNode n) {
    nb("ClassNode")
    p("name", n.name)
    p("modifiers", n.modifiers, "int", modifier2str(n.modifiers))
    p("superClass", n.superClass.name)

    if (n.fields?.size() > 0) {
      pbl "fields",n.fields?.size()
      n.fields?.each{
        visitField(it)
//        it.visit(this)
      }
      pel()
    } else {
      pl "fields"
    }
    if (n.methods?.size() > 0) {
      pbl "methods",n.methods?.size()
      n.methods?.each{
        visitMethod(it)
      }
      pel()
    } else {
      pl "methods"
    }

    if (n.properties?.size() > 0) {
      pbl "properties",n.properties?.size()
      n.properties?.each{
        visitProperty(it)
      }
      pel()
    } else {
      pl "properties"
    }

    if (n.declaredConstructors?.size()) {
      pbl "declaredConstructors",n.declaredConstructors?.size()
      n.declaredConstructors?.each{
        visitConstructor(it)
      }
      pel()
    } else {
      pl "declaredConstructors"
    }

    if (n.objectInitializerStatements?.size()) {
      pbl "objectInitializerStatements",n.objectInitializerStatements.size()
      n.dobjectInitializerStatements.each{ it.visit(this) }
      pel()
    } else {
      pl "objectInitializerStatements"
    }


    ne()
  }

  void visitMethod(MethodNode n) {
    nb("MethodNode")
    p("name", n.name)
    p("modifiers", n.modifiers, "int", modifier2str(n.modifiers))
    p("returnType", trimType(n.returnType))
    if (n.parameters?.size() > 0) {
      pbl("parameters",n.parameters?.size())
      n.parameters.each {visitParameter(it)}
      pel()
    } else {
      pl("parameters")
    }
    pb("code")
    n.code.visit(this)
    pe()
    ne()
  }

  void  visitArgumentlistExpression(ArgumentListExpression expression) {
    nb "ArgumentListExpression"
    if (expression?.expressions?.size() > 0) {
      pbl "expressions",expression?.expressions?.size()
      expression?.expressions?.each {
        it.visit(this)
      }
      pel ()
    } else {
      pl "expressions"
    }
    ne ()
  }
  void  visitArrayExpression(ArrayExpression expression) {
    nb "ArrayExpression"
    if (expression.expressions?.size() >0) {
      pbl "expressions",expression.expressions?.size()
      expression.expressions.each { it.visit(this) }
      pel()
    } else {
      pl "expressions"
    }
    ne()
  }
  void  visitAssertStatement(AssertStatement statement) {
    nb "AssertStatement"
    pb "booleanExpression"
    statement.booleanExpression.visit(this)
    pe()
    pb "messageExpression"
    statement.messageExpression.visit(this)
    pe()
    ne()
  }
  void  visitAttributeExpression(AttributeExpression attributeExpression) {
    nb "AttributeExpression"
    pb "objectExpression"
    attributeExpression.objectExpression.visit(this)
    pe ()
    pb "property"
    attributeExpression.property.visit(this)
    pe ()
    ne ()
  }
  void  visitBinaryExpression(BinaryExpression expression) {
    nb "BinaryExpression"
    pb "leftExpression"
    expression.leftExpression.visit(this)
    pe()
    pb "operation"
    visitToken(expression.operation)
    pe()
    pb "rightExpression"
    expression.rightExpression.visit(this)
    pe()
    ne()
  }
  void  visitBitwiseNegationExpression(BitwiseNegationExpression expression) {
    pb "leftExpression"
    expression.expression.visit(this)
    pe()
    nb "BitwiseNegationExpression"
    ne()

  }
  void  visitBlockStatement(BlockStatement statement) {
    nb "BlockStatement"
    if (statement.statements?.size() > 0) {
      pbl "statements",statement.statements?.size()
      statement.statements.each {
        it.visit(this)
      }
      pel()
    } else {
      pl "statements"
    }
    ne()
  }
  void  visitBooleanExpression(BooleanExpression expression) {
    nb "BooleanExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitBreakStatement(BreakStatement statement) {
    nb "BreakStatement"
    ne()
  }
  void  visitBytecodeExpression(BytecodeExpression expression) {
    nb "BytecodeExpression"
    ne()
  }
  void  visitCaseStatement(CaseStatement statement) {
    nb "CaseStatement"
    pb "expression"
    statement.expression.visit(this)
    pe()
    pb "code"
    statement.code.visit(this)
    pe()
    ne()
  }
  void  visitCastExpression(CastExpression expression) {
    nb "CastExpression"
    p "type", trimType(expression.type)
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitCatchStatement(CatchStatement statement) {
    nb "CatchStatement"
    p "variable",statement.variable
    pb "code"
    statement.code.visit(this)
    pe()
    ne()
  }
  void  visitClassExpression(ClassExpression expression) {
    nb "ClassExpression"
    p "type",trimType(expression.type)
    ne()
  }
  void  visitClosureExpression(ClosureExpression expression) {
    nb "ClosureExpression"
    if (expression.parameters.size()>0) {
      pbl "parameters",expression.parameters.size()
      expression.parameters.each {
        visitParameter(it)
      }
      pel()
    } else {
      pl "parameters"
    }
    pb "code"
    expression.code.visit(this)
    pe()
    ne()
  }
  void  visitClosureListExpression(ClosureListExpression closureListExpression) {
    nb "ClosureListExpression"
    if (closureListExpression.expressions?.size() > 0) {
      pbl "expressions",closureListExpression.expressions?.size()
      closureListExpression.expressions.each { it.visit(this) }
      pel()
    } else {
      pl "expressions"
    }
    ne()
  }
  void  visitConstantExpression(ConstantExpression expression) {
    nb "ConstantExpression"
    p "value",expression.value
    p "type",trimType(expression?.value?.class)
    ne()
  }
  void  visitConstructorCallExpression(ConstructorCallExpression expression) {
    nb "ConstructorCallExpression"
    p "type",trimType(expression.type)
    p "methodAsString",expression.methodAsString
    p "specialCall",expression.isSpecialCall()
    p "superCall",expression.isSuperCall()
    p "thisCall",expression.isThisCall()
    p "usingAnonymousInnerClass",expression.isUsingAnonymousInnerClass()
    pb "arguments"
    expression.arguments.visit(this)
    pe()
    ne()
  }
  void  visitContinueStatement(ContinueStatement statement) {
    nb "ContinueStatement"
    ne()
  }
  void  visitDeclarationExpression(DeclarationExpression expression) {
    nb "DeclarationExpression"
    pb "leftExpression"
    expression.leftExpression.visit(this)
    pe()
    pb "operation"
    visitToken(expression.operation)
    pe()
    pb "rightExpression"
    expression.rightExpression.visit(this)
    pe()
    ne()
  }
  void  visitDoWhileLoop(DoWhileStatement loop) {
    nb "DoWhileStatement"
    pb "booleanExpression"
    loop.booleanExpression.visit(this)
    pe()
    pb "loopBlock"
    loop.loopBlock.visit(this)
    pe()
    ne()
  }
  void  visitExpressionStatement(ExpressionStatement statement) {
    nb "ExpressionStatement"
    pb "expression"
    statement.expression.visit(this)
    pe()
    ne()
  }
  void  visitFieldExpression(FieldExpression expression) {
    nb "FieldExpression"
    p "fieldName",expression.fieldName
    ne()
  }
  void  visitForLoop(ForStatement forLoop) {
    nb "ForStatement"
    pb "variable"
    visitParameter(forLoop.variable)
    pe()
    pb "collectionExpression"
    forLoop.collectionExpression.visit(this)
    pe()
    pb "loopBlock"
    forLoop.loopBlock.visit(this)
    pe()
    ne()
  }
  void  visitGStringExpression(GStringExpression expression) {
    nb "GStringExpression"
    if (expression.strings?.size() > 0) {
      pbl "strings",expression.strings?.size()
      expression.strings.each { it.visit(this) }
      pel()
    } else {
      pl "strings"
    }
    if (expression.values?.size() > 0) {
      pbl "values",expression.values?.size()
      expression.values.each { it.visit(this) }
      pel()
    } else {
      pl "values"
    }
    ne()
  }
  void  visitIfElse(IfStatement ifElse) {
    nb "IfStatement"
    pb "booleanExpression"
    ifElse.booleanExpression.visit(this)
    pe()
    pb "ifBlock"
    ifElse.ifBlock.visit(this)
    pe()
    pb "elseBlock"
    if (! ifElse.elseBlock.isEmpty()) {
      ifElse.elseBlock.visit(this)
    } else {
      nb "EmptyStatement"
      ne()
    }
    pe()
    ne()
  }
  void  visitListExpression(ListExpression expression) {
    nb "ListExpression"
    if (expression.expressions?.size() > 0) {
      pbl "expressions",expression.expressions?.size()
      expression.expressions.each { it.visit(this) }
      pel()
    } else {
      pl "expressions"
    }
    ne()
  }
  void  visitMapEntryExpression(MapEntryExpression expression) {
    nb "MapEntryExpression"
    pb "keyExpression"
    expression.keyExpression.visit(this)
    pe()
    pb "valueExpression"
    expression.valueExpression.visit(this)
    pe()
    ne()
  }
  void  visitMapExpression(MapExpression expression) {
    nb "MapExpression"
    if (expression.mapEntryExpressions.size() > 0) {
      pbl "mapEntryExpressions",expression.mapEntryExpressions.size()
      expression.mapEntryExpressions.each { it.visit(this) }
      pel()
    } else {
      pl "mapEntryExpressions"
    }
    ne()
  }
  void  visitMethodCallExpression(MethodCallExpression call) {
    nb "MethodCallExpression"
    pb "objectExpression"
    call.objectExpression.visit(this)
    pe()
    pb "method"
    call.method.visit(this)
    pe()
    pb "arguments"
    call.arguments.visit(this)
    pe()
    ne()
  }
  void  visitMethodPointerExpression(MethodPointerExpression expression) {
    nb "MethodPointerExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    pb "methodName"
    expression.methodName.visit(this)
    pe()
    ne()
  }
  void  visitNotExpression(NotExpression expression) {
    nb "NotExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitPostfixExpression(PostfixExpression expression) {
    nb "PostfixExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    pb "operation"
    visitToken(expression.operation)
    pe()
    ne()
  }
  void  visitPrefixExpression(PrefixExpression expression) {
    nb "PrefixExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    pb "operation"
    visitToken(expression.operation)
    pe()
    ne()
  }
  void  visitPropertyExpression(PropertyExpression expression) {
    nb "PropertyExpression"
    pb "objectExpression"
    expression.objectExpression.visit(this)
    pe()
    pb "property"
    expression.property.visit(this)
    pe()
    ne()
  }
  void  visitRangeExpression(RangeExpression expression) {
    nb "RangeExpression"
    p "inclusive",expression.inclusive
    pb "from"
    expression.from.visit(this)
    pe()
    pb "to"
    expression.to.visit(this)
    pe()
    ne()
  }
  void  visitReturnStatement(ReturnStatement statement) {
    nb "ReturnStatement"
    pb "expression"
    statement.expression.visit(this)
    pe()
    ne()
  }
  void  visitShortTernaryExpression(ElvisOperatorExpression expression) {
    nb "ElvisOperatorExpression"
    pb "booleanExpression"
    expression.booleanExpression.visit(this)
    pe()
    pb "trueExpression"
    expression.trueExpression.visit(this)
    pe()
    pb "falseExpression"
    expression.falseExpression.visit(this)
    pe()
    ne()
  }
  void  visitSpreadExpression(SpreadExpression expression) {
    nb "SpreadExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitSpreadMapExpression(SpreadMapExpression expression) {
    nb "SpreadMapExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void visitStaticMethodCallExpression(StaticMethodCallExpression expression) {
    nb "StaticMethodCallExpression"
    p "type",trimType(expression.type)
    p "method",expression.method
    pb "arguments"
    call.arguments.visit(this)
    pe()
    ne()
  }
  void  visitSwitch(SwitchStatement statement) {
    nb "SwitchStatement"
    pb "expression"
    statement.expression.visit(this)
    pe()
    if (statement.caseStatements.size() > 0) {
      pbl "caseStatements",statement.caseStatements.size()
      statement.caseStatements.each { it.visit(this) }
      pel()
    } else {
      pl "caseStatements"
    }
    pb "defaultStatement"
    statement.defaultStatement.visit(this)
    pe()
    ne()
  }
  void  visitSynchronizedStatement(SynchronizedStatement statement) {
    nb "SynchronizedStatement"
    pb "expression"
    expression.expression.visit(this)
    pe()
    pb "code"
    expression.code.visit(this)
    pe()
    ne()
  }
  void  visitTernaryExpression(TernaryExpression expression) {
    nb "TernaryExpression"
    pb "booleanExpression"
    expression.booleanExpression.visit(this)
    pe()
    pb "trueExpression"
    expression.trueExpression.visit(this)
    pe()
    pb "falseExpression"
    expression.falseExpression.visit(this)
    pe()
    ne()
  }
  void  visitThrowStatement(ThrowStatement statement) {
    nb "ThrowStatement"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitTryCatchFinally(TryCatchStatement finally1) {
    nb "TryCatchStatement"
    pb "tryStatement"
    finally1.tryStatement.visit(this)
    pe()
    if (finally1.catchStatements.size() > 0) {
      pbl "catchStatement",finally1.catchStatements.size()
      finally1.catchStatements.each { it.visit(this) }
      pel()
    } else {
      pl "catchStatement"
    }
    if (finally1.finallyStatement) {
      pb "finallyStatement"
      finally1.finallyStatement.visit(this)
      pe()
    }
    ne()
  }
  void  visitTupleExpression(TupleExpression expression) {
    nb "TupleExpression"
    if (expression.expressions?.size()>0) {
      pbl "expressions",expression.expressions?.size()
      expression.expressions.each { it.visit(this) }
      pel()
    } else {
      pl "expressions"
    }
    ne()
  }
  void  visitUnaryMinusExpression(UnaryMinusExpression expression) {
    nb "UnaryMinusExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitUnaryPlusExpression(UnaryPlusExpression expression) {
    nb "UnaryPlusExpression"
    pb "expression"
    expression.expression.visit(this)
    pe()
    ne()
  }
  void  visitVariableExpression(VariableExpression expression) {
    nb "VariableExpression"
    p "name",expression.name
    ne()
  }
  void  visitWhileLoop(WhileStatement loop) {
    nb "WhileStatement"
    pb "booleanExpression"
    loop.booleanExpression.visit(this)
    pe()
    pb "loopBlock"
    loop.loopBlock.visit(this)
    pe()
    ne()
  }
}

*1:http://www.graphviz.org/