最近作的兩個項目,一個是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行重複的代碼,能夠用一個函數複用,但前幾天看了王垠的《編程的智慧》,感受這個代碼複用一個函數不太直觀,有點操做過分,因而就保留上面這樣了。