post Image
Goの抽象構文木(AST)を手入力してHello, Worldを作る #golang

はじめに

タイトルを見て、「はて?何を言ってるんだろう」と思った方もいるでしょう。
その通りです。通常、抽象構文木(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構造体は、importvarconsttypeなどを定義するノードを表し、以下のようなフィールドを持ちます。

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を持ちます。

この場合は、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を手入力してみて、どういう構造になっているのか実感してみてください。


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。