go/typesのEval()でリテラルを扱った数値計算のその先

go/types関連の関数などの使い方をメモしておくと便利なのでメモ。おそらく一番わかり易い例として電卓の様な振る舞いをするようなものを作ってみる。

かんたんな数式の計算(電卓)

たまにgo/typesのEval()に触れる記事の中でpkgにnilを渡して数式をする例をあげることがある。以下の様な感じ。 (ここまでだけで終わっている記事があったりしてちょっとさみしい)

package main

import (
    "fmt"
    "go/token"
    "go/types"
    "log"
)

func main() {
    fset := token.NewFileSet()
    expr := "1 * 2 + 3 / 4.0"
    tv, err := types.Eval(fset, nil, token.NoPos, expr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr, "=", tv.Value)
    // 1 * 2 + 3 / 4.0 = 2.75
}

これだけだと結局リテラルを計算するだけなので微妙。

pkgにnilを渡さないで変数を使う

pkgに *types.Package の値を渡してあげる。pkgというのはふつうにgoのコードを書いているときの package <package name> と同じ概念。packageの中にScopeというフィールドを持っており、そこに値を格納しておける。

package main

import (
    "fmt"
    "go/constant"
    "go/token"
    "go/types"
    "log"
)

func main() {
    fset := token.NewFileSet()
    pkg := types.NewPackage("<dummy>", "p")

    // const x = int64(10) とほとんど同じ
    inttype := types.Universe.Lookup("int64").Type()
    x := inttype, constant.MakeInt64(10)
    pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, "x", x))

    expr := "x * x"

    tv, err := types.Eval(fset, pkg, token.NoPos, expr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr, "=", tv.Value)
    // x * x = 100
}

xという変数を利用した電卓のようなことができた。

事前に定義しておいた定数を使った電卓

go/typesにはConfigという便利オブジェクトがいて、このConfigのCheck()を実行した結果がpackageとなって返ってくる。これを利用して事前に定義しておいた定数を使って計算することもできる(名前から推測できるように色々チェックしてくれる。エラーが発生した場合にはErrorフィールドに渡した関数を呼んでくれる)。

package main

import (
    "fmt"
    "go/ast"
    "go/importer"
    "go/parser"
    "go/token"
    "go/types"
    "log"
)

func main() {
    fset := token.NewFileSet()

    code := `
package p
const x = 10
`
    conf := types.Config{
        Importer: importer.Default(),
        Error: func(err error) {
            fmt.Printf("!!! %#v\n", err)
        },
    }

    // load
    file, err := parser.ParseFile(fset, "p", code, parser.AllErrors)
    pkg, err := conf.Check("p", fset, []*ast.File{file}, nil)
    if err != nil {
        log.Fatal(err)
    }

    // eval
    expr := "x * x"

    tv, err := types.Eval(fset, pkg, token.NoPos, expr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr, "=", tv.Value)
    // x * x = 100
}

もちろん。ここで計算した x * x の値をscopeに格納して利用する事もできる。 y = x * x として利用する場合には以下の様にして書く。

   // y = x * x
    pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, "y", tv.Type, tv.Value))
    expr2 := "x + y"
    tv2, err := types.Eval(fset, pkg, token.NoPos, expr2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr2, "=", tv2.Value)
    // x + y = 110

その先

その他 go/types 以下には LookupFieldOrMethod() という名前の関数だったり ConvertibleTo(), AssignableTo() と言った名前の関数がある。これらは名前からわかるとおり、渡された値が代入可能であるかだったり変換可能であったりを調べられる。あるいはフィールドやメソッドを持っているかどうか。

これらを使ってASTをparseして変換以外の便利な何かができないかということを考えたりしていた。 (例えば、pkg上でメソッドの有無を調べてAST上にdefault実装を付加だったり。ある型から別の型への変換関数を気軽に生成できないかなどを考えたりしていた)