Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

タイトルを見て、「はて?何を言ってるんだろう」と思った方もいるでしょう。
その通りです。通常、抽象構文木(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を手入力してみて、どういう構造になっているのか実感してみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした