Goの抽象構文木(AST)を手入力してHello, Worldを作る #golang
January 3, 2017
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
タイトルを見て、「はて?何を言ってるんだろう」と思った方もいるでしょう。
その通りです。通常、抽象構文木(AST)を取得するには、「ASTを取得する方法を調べる」で解説したように、go/parser
パッケージの関数を使ってソースコードをパースする必要があります。
しかし、この記事では温かみのある手入力をすることで、日頃なんとなく取得しているASTがどういうノードで構築されているのか、最低限必要なフィールドは何なのかということを改めて知ることを目的としています。
なお、この記事を書いた時のGoの最新バージョンは1.7.4です。
今回作るコード
今回はかのプログラム言語Cやプログラミング言語Goで有名なHello, World
を出力するプログラムを作りたいと思います。
具体的には、以下のようなコードです。
なお、せっかくなので、ここではGoっぽく、Hello, 世界
としています。
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
ast.File
を作る
この記事では、1つのファイルから構成されるコードを作るので、ast.File
構造体を作っていきます。
ast.File
構造体は以下のようなフィールドを持ちます。
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
この中で今回作成するコードに必須なものはどれでしょうか?
Name
フィールドはいりそうですね。
あとは、Decls
フィールドは、パッケージのインポートとか関数の定義とかで必要になりそうです。
token.Pos
方のフィールドは、きっとフォーマッターがよしなに位置を決めてくれるから無視しても良さそうです。
インポートする
fmt
パッケージをインポートする部分をASTで書いてみましょう。
インポートは、ast.GenDecl
構造体で表すことができます。
ast.GenDecl
構造体は、import
やvar
、const
、type
などを定義するノードを表し、以下のようなフィールドを持ちます。
type GenDecl struct {
Doc *CommentGroup // associated documentation; or nil
TokPos token.Pos // position of Tok
Tok token.Token // IMPORT, CONST, TYPE, VAR
Lparen token.Pos // position of '(', if any
Specs []Spec
Rparen token.Pos // position of ')', if any
}
なお、Tok
フィールドは以下の4種類の値のうちどれかで、それぞれは対応するast.Spec
を持ちます。
token.IMPORT
:*ast.ImportSpec
token.CONST
:*ast.ValueSpec
token.TYPE
:*ast.TypeSpec
token.VAR
:*ast.ValueSpec
この場合は、import
なので、Tok
フィールドはtoken.IMPORT
になり、用いるast.Spec
インタフェースを満たす型はast.ImportSpec
構造体のポインタとなります。
ここまでのコードをまとめると、インポートのast.GenDecl
の初期化は以下のようになります。
&ast.GenDecl{
Tok: token.IMPORT,
Specs: []ast.Spec{
&ast.ImportSpec{
// TODO: フィールドを埋める
},
},
}
さて、ast.ImportSpec
構造体のフィールドはどう初期化すればよいでしょうか?
ast.ImportSpec
構造体は以下のようなフィールドを持ちます。
type ImportSpec struct {
Doc *CommentGroup // associated documentation; or nil
Name *Ident // local package name (including "."); or nil
Path *BasicLit // import path
Comment *CommentGroup // line comments; or nil
EndPos token.Pos // end of spec (overrides Path.Pos if nonzero)
}
Name
フィールドは、インポートした際につけるファイルスコープで有効な別名でしょう。
Path
フィールドには、インポートパスを書けば良さそうです。
ast.BasicLit
構造体のポインタ型の値をとるようなので、以下のようなに文字列を指定すれば良さそうです。
&ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote("fmt"),
},
}
この時、Value
フィールドには、""
でくくられた文字列を指定する必要があるため、strconv
パッケージのstrconv.Quote
関数を用いる必要があります。
これでfmt
パッケージをインポートすることができました。
main
関数を作る
続いてmain
関数を作りましょう。
関数定義も、ast.File
構造体のDecls
フィールドの要素として、ast.FuncDecl
構造体のポインタ型の値を設定しておけば良さそうです。
ast.FuncDecl
構造体には以下のようなフィールドがあります。
type FuncDecl struct {
Doc *CommentGroup // associated documentation; or nil
Recv *FieldList // receiver (methods); or nil (functions)
Name *Ident // function/method name
Type *FuncType // function signature: parameters, results, and position of "func" keyword
Body *BlockStmt // function body; or nil (forward declaration)
}
今回はメソッドではないため、Recv
フィールドは無視して良さそうです。
関数名をName
フィールドで指定し、関数の本体はBody
フィールドで指定すれば良さそうです。
また、Type
フィールドには、ast.FuncType
構造体のポインタ型の値として、シグニチャを設定する必要があります。
ast.FuncType
構造体には、以下のようなフィールドがあります。
type FuncType struct {
Func token.Pos // position of "func" keyword (token.NoPos if there is no "func")
Params *FieldList // (incoming) parameters; non-nil
Results *FieldList // (outgoing) results; or nil
}
今回のmain
関数は、引数も戻り値もないので、何もフィールドは指定しなくても良さそうです。
ここまでのコードをまとめると以下のようになります。
&ast.FuncDecl{
Name: ast.NewIdent("main"),
Type: &ast.FuncType{},
Body: /* TODO: 本体を設定する */,
}
関数の本体、つまり、{
と}
で囲まれた部分には、複文が設定されます。
複文は、ast.BlockStmt
構造体で表現されます。
ast.BlockStmt
構造体は以下のようなフィールドで構成されます。
type BlockStmt struct {
Lbrace token.Pos // position of "{"
List []Stmt
Rbrace token.Pos // position of "}"
}
複文は文の集まりなので、List
フィールドに文を表すast.Stmt
構造体のポインタ型のスライスを設定すれば良さそうです。
今回は、fmt.Println("Hello, 世界")
だけなので、文は1つだけで良さそうです。
fmt.Println("Hello, 世界")
を呼び出す
さて、fmt
パッケージのPrintln
メソッドを呼び出す部分を書いていきましょう。
この文は、式のみで構成されるのでast.ExprStmt
構造体を用いましょう。
ast.ExprStmt
構造体は、式だけからなる文で以下のようなフィールドを用います。
type ExprStmt struct {
X Expr // expression
}
見て分かるとおり、式を表すast.Expr
インタフェースをX
フィールドとして保持しているだけです。
fmt.Println
関数を呼び出すため、式の種類としては関数呼び出しを表すast.CallExpr
構造体を用います。
ast.CallExpr
構造体は以下のフィールドを持ちます。
type CallExpr struct {
Fun Expr // function expression
Lparen token.Pos // position of "("
Args []Expr // function arguments; or nil
Ellipsis token.Pos // position of "...", if any
Rparen token.Pos // position of ")"
}
Fun
フィールドには、関数を参照するための式が入ります。
具体的には、関数名を表す*ast.Ident
型やパッケージ関数やメソッドを表す*ast.SelectorExpr
、関数リテラルを表す*ast.FuncLit
が設定されます。
今回は、パッケージ関数を呼び出したいので、ast.SelectorExpr
構造体を用います。
ast.SelectorExpr
構造体は以下のようなフィールドで構成されています。
type SelectorExpr struct {
X Expr // expression
Sel *Ident // field selector
}
X
フィールドには、パッケージ名やレシーバを指定し、Sel
フィールドには関数名やメソッド名を指定します。
さて、ここでもう一度ast.CallExpr
構造体のフィールドをみてみましょう。
type CallExpr struct {
Fun Expr // function expression
Lparen token.Pos // position of "("
Args []Expr // function arguments; or nil
Ellipsis token.Pos // position of "...", if any
Rparen token.Pos // position of ")"
}
fmt.Println
関数の引数として、"Hello, 世界"
を指定する必要があるため、ast.CallExpr
構造体のArgs
フィールドを設定する必要があります。
"Hello, 世界"
は文字列なため、ast.BasicLit
を用いれば良さそうです。
ここまでの処理をコードにまとめると以下のようになります。
&ast.ExprStmt{
X: &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: ast.NewIdent("fmt"),
Sel: ast.NewIdent("Println"),
},
Args: []ast.Expr{
&ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote("Hello, 世界"),
},
},
},
}
ASTをコードにする
さて、これでfmt
パッケージをインポートし、fmt.Println
関数を呼び出すmain
関数を作ることができました。
それでは次に、ASTをコードとして出力してみましょう。
コードとして出力するには、「抽象構文木(AST)をいじってフォーマットをかける 」という記事で解説した、go/format
パッケージのformat.Node
関数を用いれば良さそうです。
format.Node(os.Stdout, token.NewFileSet(), f)
なお、token.FileSet
はここでは後で使用しないので、引数に直接渡しています。
さて、ここまでのすべての処理をまとめると以下のようのなコードになります。
package main
import (
"go/ast"
"go/format"
"go/token"
"os"
"strconv"
)
func main() {
f := &ast.File{
Name: ast.NewIdent("main"),
Decls: []ast.Decl{
&ast.GenDecl{
Tok: token.IMPORT,
Specs: []ast.Spec{
&ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote("fmt"),
},
},
},
},
&ast.FuncDecl{
Name: ast.NewIdent("main"),
Type: &ast.FuncType{},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.ExprStmt{
X: &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: ast.NewIdent("fmt"),
Sel: ast.NewIdent("Println"),
},
Args: []ast.Expr{
&ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote("Hello, 世界"),
},
},
},
},
},
},
},
},
}
format.Node(os.Stdout, token.NewFileSet(), f)
}
出力結果もみてみましょう。 なお、このコードはThe Go Playgroundでも動かすことができます。
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
うまく目的のコードが出力できましたね。
おわりに
今回は、ASTをひとつずつ構築していくことで、Hello, World
プログラムがどのようなノードで構成されているか解説しました。
みなさんもぜひもっと複雑なコードのASTを手入力してみて、どういう構造になっているのか実感してみてください。