Scala的抽象语法树打印小工具-小拉达
為什么80%的碼農都做不了架構師?>>> ??
最近做的兩個項目,一個是VeriScala,另一個是Lickitung,都涉及到了Scala的抽象語法樹(AST),前者是寫macro的需要,后者是做AST的pattern match。
但是在網上竟沒有發現一個很好的格式化打印AST的工具。唯一找到的是ScalaAstPrinter,然而用法和輸出都不太符合我的期望,不知道是這個需求太小還是我走錯方向了。于是自己寫了一個。因為只有幾十行代碼并且是個很小的工具,于是取名叫Rattata,口袋妖怪中的小拉達。
項目地址:https://github.com/Azard/Rattata
大三的時候編譯原理大作業也做過一個樹狀的AST輸出,當時前端顯示的部分是_guoyanchang_寫的,他現在在阿里云搬磚。
當時_guoyanchang_實現的樹狀打印輸入是一個我傳給他的Java實現的多叉樹的數據結構,現在的情況是Scala的抽象語法樹經過showRaw處理后的一個字符串。
val exp = reify {val x = 1val y = 2x + 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 = 0showRaw(input).foreach {case '(' =>level += 1println()if (showLine) {print(("|" + " "*(tabSize-1)) * (level-1))print("|" + "-"*(tabSize-1))} else {print(" " * tabSize * level)}case ')' =>level -= 1case ',' =>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)得到了如下的輸出:
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為止。
加入Expr作為0級,Block作為1級,這里的主要問題是在邊讀邊輸出的時候不知道后面是否還要某一級的token,如果我輸出完Block知道了后面的字符串沒有第1級的token,我就可以不打印Expr下的直線。
于是我又想到了新的實現,先讀第一遍根據(和)統計各個級的token數量,然后讀第二遍再邊讀邊輸出,當輸出完第n級的一個token時在第n級的總token數上減1,這樣就可以去掉所有多余的線。
仔細想一想,似乎是沒有辦法做到只讀一遍就不打印多余的線的,因為這需要知道整個抽象語法樹的狀態,必須先讀一遍存好狀態,第二遍根據狀態輸出。
然而這個多余的輸出似乎更好看點,因為可以直接看到后面的token是第幾級的,看起來更直觀。我問了問_tcbbd_,他也覺得保留多余的線比較好,于是這個Rattata就陰差陽錯的用上面那種方式打印Scala AST。
最終的實現case '('和case ','有7行重復的代碼,可以用一個函數復用,但前幾天看了王垠的《編程的智慧》,感覺這個代碼復用一個函數不太直觀,有點操作過度,于是就保留上面這樣了。
轉載于:https://my.oschina.net/azard/blog/538131
總結
以上是生活随笔為你收集整理的Scala的抽象语法树打印小工具-小拉达的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# IOCP完成端口模型(简单实用高效
- 下一篇: Http 四种请求访问代码 HttpGe