wasmでGoのAST Viewerを作った話
はじめに
Goの1.11からwasm
向けのバイナリを吐けるようになりました。
wasm
上でパーサを実行し、GoのASTをビジュアライズするものを作ったので、その紹介をします。
作ったものはこちら。
AST Viewer
Goに限らない話かもしれないですが、コード自動生成や、静的解析系のツールを実装しようとする場合、ASTを扱うことがよくあると思います。
ASTを直感的に扱えるようになるためには相当な慣れが必要になり、はじめのうちは、コードがどういったASTにパースされるのかを把握するために、逐一print
するなどして確認していました。
この作業の効率を上げるために、ASTのvisualizeをするツールをせっかくなので自分で作ってみることにしました。
というわけで、作ったものがこちらです。
editor部分はmonaco editor、ASTをパースする部分はGoのwasmで実装されています。 server sideでは何もしておらず、静的ファイルを返すだけの実装となっているので、hostingさえしてしまえば、browser上で実行することができます。
実行方法
wasm
やjs
はすべてstatikで固めているので、
$ go get -u github.com/akito0107/astviewer/cmd/astviewer
で手元にインストールすることができます。手元でビルドしたい場合は、
Go, dep, Node.js, yarnを入れた上で、
$ git clone https://github.com/akito0107/astviewer/cmd/astviewer $ cd ${clone-repo} $ make
とうつとbin
配下にバイナリが生成されると思います。
コマンドを実行すると、サーバが起動します。
$ astviewer
デフォルトでは8080
でlistenしますが、-p
オプションでportを任意に設定できます。
ブラウザにアクセスすると、添付の画像のような画面にアクセスできると思います。
wasmの実装
wasm部分の実装を簡単に見ていきます。Goのwasmについてはここを参照してください。
これがwasmの実装になっています。
// +build js,wasm package main import ( "bytes" "fmt" "go/ast" "go/parser" "go/token" "log" "syscall/js" ) // 中略 func main() { gosrc := js.Global().Get("document").Call("getElementById", "gosrc") src := gosrc.Get("value").String() goout := js.Global().Get("document").Call("getElementById", "goout") var buf bytes.Buffer var depth int fset := token.NewFileSet() f, err := parser.ParseFile(fset, "", src, parser.ParseComments) if err != nil { fmt.Printf("%+v\n", err) return } ast.Inspect(f, func(n ast.Node) bool { if n != nil { depth-- } else { depth++ return true } lineno := fset.Position(n.Pos()).Line view := &ASTView{ LineNo: lineno, } switch x := n.(type) { case *ast.ArrayType: view.Label = "ArrayType" view.Value = fmt.Sprintf("Len: %s, Elt: %#v", x.Len, x.Elt) case *ast.AssignStmt: view.Label = "AssignStmt" view.Value = fmt.Sprintf("Lhs: %#v, Tok: %d, Rhs: %#v", x.Lhs, x.Tok, x.Rhs) /* ~中略~ */ default: view.Label = "Unknown" } if _, err := fmt.Fprintf(&buf, "<span class=\"astline lineno%d\">%*s%s</span>\n", view.LineNo, depth*2, "", view); err != nil { log.Printf("%+v", err) return false } return true }) goout.Set("innerHTML", buf.String()) }
syscall/js
をimport
することにより、js.Global
などの関数が使えるようになります。
下記のように、直感的にDOMの値を取得および操作することができるようになります。
gosrc := js.Global().Get("document").Call("getElementById", "gosrc") src := gosrc.Get("value").String() // 中略 goout := js.Global().Get("document").Call("getElementById", "goout") // 中略 goout.Set("innerHTML", buf.String())
Examplesは少ないですが、公式のdocも充実しているので、参照してみてください。
ASTをparseする際のtipsとして、token.FileSet
を用いることにより、Nodeの行数を取得することができるようになります。
これにより、ASTをbufferに書き出す際に、<span class="lineno{行数}">
のclassを付与し、editorのcursorの行数の背景色をリアルタイムに変えています。
fset := token.NewFileSet() // ParseFileの第1引数に渡す f, err := parser.ParseFile(fset, "", src, parser.ParseComments) ast.Inspect(f, func(n ast.Node) bool { // ast.NodeのPos()をPosition()に渡すとASTのPositionを手に入れられる lineno := fset.Position(n.Pos()).Line // 以下略
まとめ
サーバ側のプロセスでparseを行わないでも、browser上のwasm
でGoのパーサを実行することにより、ASTをvisualizeすることができました。
自分自身もASTをパースして静的解析や自動生成のツールを書くことが多いので、便利になるようなツールを作ってみました。
今後はよりわかりやすい見た目にしていきたいと思います!