asterisc

技術ネタ中心にその他雑多なこと

wasmでGoのAST Viewerを作った話

はじめに

Goの1.11からwasm向けのバイナリを吐けるようになりました。 wasm上でパーサを実行し、GoのASTをビジュアライズするものを作ったので、その紹介をします。 作ったものはこちら。

github.com

AST Viewer

Goに限らない話かもしれないですが、コード自動生成や、静的解析系のツールを実装しようとする場合、ASTを扱うことがよくあると思います。 ASTを直感的に扱えるようになるためには相当な慣れが必要になり、はじめのうちは、コードがどういったASTにパースされるのかを把握するために、逐一printするなどして確認していました。 この作業の効率を上げるために、ASTのvisualizeをするツールをせっかくなので自分で作ってみることにしました。

というわけで、作ったものがこちらです。

editor部分はmonaco editor、ASTをパースする部分はGoのwasmで実装されています。 server sideでは何もしておらず、静的ファイルを返すだけの実装となっているので、hostingさえしてしまえば、browser上で実行することができます。

実行方法

wasmjsはすべて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/jsimportすることにより、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をパースして静的解析や自動生成のツールを書くことが多いので、便利になるようなツールを作ってみました。 今後はよりわかりやすい見た目にしていきたいと思います!