最近作的两个项目,一个是VeriScala,另外一个是Lickitung,都涉及到了Scala的抽象语法树(AST),前者是写macro的须要,后者是作AST的pattern match。前端
可是在网上竟没有发现一个很好的格式化打印AST的工具。惟一找到的是ScalaAstPrinter,然而用法和输出都不太符合个人指望,不知道是这个需求过小仍是我走错方向了。因而本身写了一个。由于只有几十行代码而且是个很小的工具,因而取名叫Rattata,口袋妖怪中的小拉达。git
项目地址:https://github.com/Azard/Rattatagithub
大三的时候编译原理大做业也作过一个树状的AST输出,当时前端显示的部分是_guoyanchang_写的,他如今在阿里云搬砖。编程
当时_guoyanchang_实现的树状打印输入是一个我传给他的Java实现的多叉树的数据结构,如今的状况是Scala的抽象语法树通过showRaw
处理后的一个字符串。数据结构
val exp = reify { val x = 1 val y = 2 x + y }
scala> println(showRaw(exp)) Expr(Block(List(ValDef(Modifiers(), TermName("x"), TypeTree(), Literal(Constant(1))), ValDef(Modifiers(), TermName("y"), TypeTree(), Literal(Constant(2)))), Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Ident(TermName("y"))))))
我最开始的想法是转成多叉树结构,再想办法打印,但以为这样彷佛小题大作并且不够优雅。函数
第二个想法是将每一个token先提取出来,存在一个Array中,而后再读一遍这个字符串根据(
和)
的顺序判断入栈出栈,而后依次用不一样的缩进打印token。这样的实现首先对字符串作了屡次replace
和split
而后获得了一个token的Array,还用map
什么的去除了split
后的空格,而后再读取一遍获得入栈出栈顺序,感受上又作了多余的事。工具
而后这个时候想到彷佛能够读取一遍字符串,读到(
就入栈,读到)
就出栈,读到,
只换行。而后就获得了以下代码:阿里云
def pprintAST(input: Expr[Any]) = { var level = 0 showRaw(input).foreach { case '(' => level += 1 println() if (showLine) { print(("|" + " "*(tabSize-1)) * (level-1)) print("|" + "-"*(tabSize-1)) } else { print(" " * tabSize * level) } case ')' => level -= 1 case ',' => println() if (showLine) { print(("|" + " "*(tabSize-1)) * (level-1)) print("|" + "-"*(tabSize-1)) } else { print(" " * tabSize * level) } case ' ' => case f => print(f) } }
固然最开始的实现不包括showLine
和tabSize
相关的东西,调用Rattata.pprintAST(exp)
获得了以下的输出:scala
Expr |---Block | |---List | | |---ValDef | | | |---Modifiers | | | | |--- | | | |---TermName | | | | |---"x" | | | |---TypeTree | | | | |--- | | | |---Literal | | | | |---Constant | | | | | |---1 | | |---ValDef | | | |---Modifiers | | | | |--- | | | |---TermName | | | | |---"y" | | | |---TypeTree | | | | |--- | | | |---Literal | | | | |---Constant | | | | | |---2 | |---Apply | | |---Select | | | |---Ident | | | | |---TermName | | | | | |---"x" | | | |---TermName | | | | |---"$plus" | | |---List | | | |---Ident | | | | |---TermName | | | | | |---"y"
然而和我指望的实现仍是有点区别,这里有些多余的线,好比Expr
下的直线只须要到Block
为止。code
加入Expr
做为0级,Block
做为1级,这里的主要问题是在边读边输出的时候不知道后面是否还要某一级的token,若是我输出完Block
知道了后面的字符串没有第1级的token,我就能够不打印Expr
下的直线。
因而我又想到了新的实现,先读第一遍根据(
和)
统计各个级的token数量,而后读第二遍再边读边输出,当输出完第n级的一个token时在第n级的总token数上减1,这样就能够去掉全部多余的线。
仔细想想,彷佛是没有办法作到只读一遍就不打印多余的线的,由于这须要知道整个抽象语法树的状态,必须先读一遍存好状态,第二遍根据状态输出。
然而这个多余的输出彷佛更好看点,由于能够直接看到后面的token是第几级的,看起来更直观。我问了问_tcbbd_,他也以为保留多余的线比较好,因而这个Rattata就阴差阳错的用上面那种方式打印Scala AST。
最终的实现case '('
和case ','
有7行重复的代码,能够用一个函数复用,但前几天看了王垠的《编程的智慧》,感受这个代码复用一个函数不太直观,有点操做过分,因而就保留上面这样了。