2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DENSOAdvent Calendar 2024

Day 14

言語の裏側を知るためにGolangのASTに親しむ話

Last updated at Posted at 2024-12-13

はじめに

この記事は DENSO Advent Calendar 2024 の14日目の記事です。

これまでアプリやインフラの開発・運用をしてきて、普段当たり前に使っているツールのその裏側について、どう作られているか良く知らないなとふと感じることがありました。そんな折に、Go のコンパイラづくりに関する以下の動画を見て、言語の動く仕組みに興味が湧いてきました。

Goのプログラム処理の一端を垣間見るため、まずは言語処理系の基礎である AST (Abstract Syntax Tree) について探検してみることにしました。

コンパイラの仕組み

コンパイラは、私たちが書いた高水準言語のソースコードを、コンピュータが直接実行可能な機械語(バイナリ)へと変換するプログラムです。その過程は大まかに次のステップで構成されます。

  1. 字句解析 (Lexical Analysis)
    ソースコードを「トークン」という最小単位(キーワード、識別子、演算子、リテラルなど)に分解します。
    例: if x > 0 { return x } → if, x, >, 0, {, return, x, }

  2. 構文解析 (Syntax Analysis)
    トークン列をもとに文法規則に従って階層構造化し、AST(抽象構文木)を構築します。
    ここでソースコードは「if文の構造」「式(x > 0)の構造」といった論理的なツリーとして表されます。

  3. 意味解析 (Semantic Analysis)
    ASTを走査し、変数や関数の存在、型が正しいか、定義と参照が整合しているかなどをチェックします。

  4. 最適化 (Optimization)
    不要な計算を取り除いたり、計算順序を変えるなど、より高速な実行やコードサイズ縮小のための最適化を行います。

  5. コード生成 (Code Generation)
    最終的にターゲットとなるアーキテクチャ(x86、ARMなど)の機械語や中間言語(IR)を生成します。

これらの中でも、構文解析で得られるASTはコンパイラにおける中核的なデータ構造です。ASTを理解すれば、コンパイラがコードをどう「理解」しているかを知る入り口になるわけです。

以下は、AST(Abstract Syntax Tree)がなぜ言語の本質を表し、なぜASTを通して機械可読性が向上するのかに焦点を当てた説明です。

AST (Abstract Syntax Tree) の重要性

プログラミング言語は、人間が理解しやすいようにテキストで記述されます。たとえば、以下のような式を考えてみましょう。

x + y * z

人間の目には「xy * zを足す」式として直感的に理解できますが、コンピュータにとっては、このままでは単なる文字の並び('x', ' ', '+', ' ', 'y', ' ', '*', ' ', 'z')であり、そこには数値、演算子、優先順位といった意味情報が直接埋め込まれていません。

人間にとって読みやすいテキストと機械処理のギャップ

テキストとしてのソースコードは、人間が読むには便利です。変数名はわかりやすく、空白や改行、コメントを使って意図を表現できます。しかし、機械にとっては「テキストのまま」では以下のような問題があります。

  • 意味の境界が曖昧: 機械は「x + y * z」という文字列を、そのままでは「何が変数で、何が演算子か?」、「どこからどこまでが一つの式か?」を理解できません。
  • 優先順位や文法規則が隠れている: 人間は算術規則を知っているので「y * z+より優先して計算する」と瞬時に判断しますが、コンピュータはそのような文法・意味規則をテキストから直接推定できません。

ASTで機械可読性を向上させる

ASTを構築すると、上記のテキスト情報は明確な「構造化データ」として表現されます。先ほどの式x + y * zは、ASTでは次のようなツリー構造に変換されます。

        (+)
       /   \
    (x)    (*)
          /   \
        (y)   (z)

このASTにおいて、

  • BinaryExpr(+) という「加算演算子を表すノード」が根にある
  • 左の子ノードが Ident(x)、右の子ノードが BinaryExpr(*) という「乗算演算子を表すノード」
  • 乗算ノードの下には Ident(y)Ident(z) がぶら下がる

という明確な「意味的、構造的」な階層が定義されます。

このような構造があることで、機械は次のようなことを容易に行えます。

  1. ノード種別による処理分岐BinaryExpr(+)といったノード型を見れば「ここは加算演算を行う部分」と判断でき、Ident(x)なら「変数x」を表すと理解できます
  2. 優先順位の自然な表現+ノードの右側には*ノードが存在し、*ノードはyzを子として持っています。これにより、y * zがひとまとまりの式としてx + ...よりも先に計算されるべきことが、構造的に明示されます
  3. 機械処理の容易さ:単なる文字列ではなく、型付きの構造化データとしてコードを扱えるため、後続の最適化、型チェック、コード生成といった処理がシンプルかつ堅牢になります

この様に、ASTはテキストベースの人間可読なコードを機械が理解できる様に言語特有の「ルールと意味」を含めて変換された結果です。このため、ASTは「言語の本質」を内包するデータ構造と言えます。

GolangのASTを覗いてみる――実装編

ではここからは、実際にGo のプログラムのASTをいくつか見てみましょう。

Goにはgo/astパッケージやgo/parserパッケージが標準で用意されており、Goのソースコードを簡単にパースし、ASTとして扱うことができます。

Go プログラムファイルパスをコマンドライン引数で渡すと AST を標準出力するサンプルコードを以下に示します。

package main

import (
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
)

// 本プログラムは、指定したGoファイルをパースしてASTを表示します。
// Usage: go run main.go -file "./sample.go"

func main() {
	filePath := flag.String("file", "", "Path to the Go source file")
	flag.Parse()

	fmt.Println(*filePath)

	if *filePath == "" {
		fmt.Println("Please specify a file path with -file option.")
		os.Exit(1)
	}

	fset := token.NewFileSet()

	f, err := parser.ParseFile(fset, *filePath, nil, parser.ParseComments)
	if err != nil {
		fmt.Printf("Error parsing file: %v\n", err)
		os.Exit(1)
	}

	ast.Print(fset, f)
}

処理のフローは以下の通りです。

  1. Goファイルのパスを受け取る
    flag.String("file", "", "Path to the Go source file")で-fileフラグを定義し、コマンドライン引数から解析します。

  2. token.FileSetの生成
    token.NewFileSet()でソースコード内の位置情報(行番号やファイル内オフセット)を管理するためのFileSetを生成します。ASTを扱う際、ソースコード中の各要素がどの行・列に対応するか、このFileSetによって追跡可能になります。

  3. parser.ParseFileによるAST生成
    parser.ParseFile(fset, *filePath, nil, parser.ParseComments)を呼び出すと、指定したファイルパスのGoソースコードを構文解析し、*ast.File構造体としてASTが返されます。

  4. ast.PrintでASTを出力
    ast.Print(fset, f)を呼ぶと、AST内の構造が標準出力にツリー形式で表示されます。ノードの型や、Ident(変数名・関数名などの識別子)、FuncDecl(関数宣言)、IfStmt(if文)、ForStmt(for文)などの構成要素が確認できます。

この処理に以下のGoプログラムを渡してみます。

package main

import "fmt"

func sample() {
	a := 1 + 1
	fmt.Println(a)
}

すると、ASTのデータ構造を含む以下の結果が出力されてきます。

./sample.go
     0  *ast.File {
     1  .  Package: ./sample.go:1:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: ./sample.go:1:9
     4  .  .  Name: "main"
     5  .  }
     6  .  Decls: []ast.Decl (len = 2) {
     7  .  .  0: *ast.GenDecl {
     8  .  .  .  TokPos: ./sample.go:3:1
     9  .  .  .  Tok: import
    10  .  .  .  Lparen: -
    11  .  .  .  Specs: []ast.Spec (len = 1) {
    12  .  .  .  .  0: *ast.ImportSpec {
    13  .  .  .  .  .  Path: *ast.BasicLit {
    14  .  .  .  .  .  .  ValuePos: ./sample.go:3:8
    15  .  .  .  .  .  .  Kind: STRING
    16  .  .  .  .  .  .  Value: "\"fmt\""
    17  .  .  .  .  .  }
    18  .  .  .  .  .  EndPos: -
    19  .  .  .  .  }
    20  .  .  .  }
    21  .  .  .  Rparen: -
    22  .  .  }
    23  .  .  1: *ast.FuncDecl {
    24  .  .  .  Name: *ast.Ident {
    25  .  .  .  .  NamePos: ./sample.go:5:6
    26  .  .  .  .  Name: "sample"
    27  .  .  .  .  Obj: *ast.Object {
    28  .  .  .  .  .  Kind: func
    29  .  .  .  .  .  Name: "sample"
    30  .  .  .  .  .  Decl: *(obj @ 23)
    31  .  .  .  .  }
    32  .  .  .  }
    33  .  .  .  Type: *ast.FuncType {
    34  .  .  .  .  Func: ./sample.go:5:1
    35  .  .  .  .  Params: *ast.FieldList {
    36  .  .  .  .  .  Opening: ./sample.go:5:12
    37  .  .  .  .  .  Closing: ./sample.go:5:13
    38  .  .  .  .  }
    39  .  .  .  }
    40  .  .  .  Body: *ast.BlockStmt {
    41  .  .  .  .  Lbrace: ./sample.go:5:15
    42  .  .  .  .  List: []ast.Stmt (len = 2) {
    43  .  .  .  .  .  0: *ast.AssignStmt {
    44  .  .  .  .  .  .  Lhs: []ast.Expr (len = 1) {
    45  .  .  .  .  .  .  .  0: *ast.Ident {
    46  .  .  .  .  .  .  .  .  NamePos: ./sample.go:6:2
    47  .  .  .  .  .  .  .  .  Name: "a"
    48  .  .  .  .  .  .  .  .  Obj: *ast.Object {
    49  .  .  .  .  .  .  .  .  .  Kind: var
    50  .  .  .  .  .  .  .  .  .  Name: "a"
    51  .  .  .  .  .  .  .  .  .  Decl: *(obj @ 43)
    52  .  .  .  .  .  .  .  .  }
    53  .  .  .  .  .  .  .  }
    54  .  .  .  .  .  .  }
    55  .  .  .  .  .  .  TokPos: ./sample.go:6:4
    56  .  .  .  .  .  .  Tok: :=
    57  .  .  .  .  .  .  Rhs: []ast.Expr (len = 1) {
    58  .  .  .  .  .  .  .  0: *ast.BinaryExpr {
    59  .  .  .  .  .  .  .  .  X: *ast.BasicLit {
    60  .  .  .  .  .  .  .  .  .  ValuePos: ./sample.go:6:7
    61  .  .  .  .  .  .  .  .  .  Kind: INT
    62  .  .  .  .  .  .  .  .  .  Value: "1"
    63  .  .  .  .  .  .  .  .  }
    64  .  .  .  .  .  .  .  .  OpPos: ./sample.go:6:9
    65  .  .  .  .  .  .  .  .  Op: +
    66  .  .  .  .  .  .  .  .  Y: *ast.BasicLit {
    67  .  .  .  .  .  .  .  .  .  ValuePos: ./sample.go:6:11
    68  .  .  .  .  .  .  .  .  .  Kind: INT
    69  .  .  .  .  .  .  .  .  .  Value: "1"
    70  .  .  .  .  .  .  .  .  }
    71  .  .  .  .  .  .  .  }
    72  .  .  .  .  .  .  }
    73  .  .  .  .  .  }
    74  .  .  .  .  .  1: *ast.ExprStmt {
    75  .  .  .  .  .  .  X: *ast.CallExpr {
    76  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    77  .  .  .  .  .  .  .  .  X: *ast.Ident {
    78  .  .  .  .  .  .  .  .  .  NamePos: ./sample.go:7:2
    79  .  .  .  .  .  .  .  .  .  Name: "fmt"
    80  .  .  .  .  .  .  .  .  }
    81  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    82  .  .  .  .  .  .  .  .  .  NamePos: ./sample.go:7:6
    83  .  .  .  .  .  .  .  .  .  Name: "Println"
    84  .  .  .  .  .  .  .  .  }
    85  .  .  .  .  .  .  .  }
    86  .  .  .  .  .  .  .  Lparen: ./sample.go:7:13
    87  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    88  .  .  .  .  .  .  .  .  0: *ast.Ident {
    89  .  .  .  .  .  .  .  .  .  NamePos: ./sample.go:7:14
    90  .  .  .  .  .  .  .  .  .  Name: "a"
    91  .  .  .  .  .  .  .  .  .  Obj: *(obj @ 48)
    92  .  .  .  .  .  .  .  .  }
    93  .  .  .  .  .  .  .  }
    94  .  .  .  .  .  .  .  Ellipsis: -
    95  .  .  .  .  .  .  .  Rparen: ./sample.go:7:15
    96  .  .  .  .  .  .  }
    97  .  .  .  .  .  }
    98  .  .  .  .  }
    99  .  .  .  .  Rbrace: ./sample.go:8:1
   100  .  .  .  }
   101  .  .  }
   102  .  }
   103  .  FileStart: ./sample.go:1:1
   104  .  FileEnd: ./sample.go:8:3
   105  .  Scope: *ast.Scope {
   106  .  .  Objects: map[string]*ast.Object (len = 1) {
   107  .  .  .  "sample": *(obj @ 27)
   108  .  .  }
   109  .  }
   110  .  Imports: []*ast.ImportSpec (len = 1) {
   111  .  .  0: *(obj @ 12)
   112  .  }
   113  .  Unresolved: []*ast.Ident (len = 1) {
   114  .  .  0: *(obj @ 77)
   115  .  }
   116  .  GoVersion: ""
   117  }

たった6行のソースコードから変換されたAST、読み解いていきましょう。

  1. *ast.File がASTの入り口
    一番外側が*ast.Fileノードで、ファイル全体を表します。

    • Package: ./sample.go:1:1package mainが定義されている位置情報が示されています。
    • Name: "main" から、パッケージ名がmainであることがわかります。
  2. インポート宣言 (*ast.GenDecl)
    Declsフィールドはファイル直下の宣言をリストで持っています。

    • 行7あたりから始まる *ast.GenDeclimport "fmt" を表します。
      Specs配下に*ast.ImportSpecがあり、Path"fmt"を示しています。
  3. 関数宣言 (*ast.FuncDecl)
    Declsの2番目(行23あたり)にある *ast.FuncDeclfunc sample() {...} を表します。

    • Name: "sample" から、この関数がsampleという名前であることがわかります。
    • Type: *ast.FuncType 内に Params フィールドが空(()のみ)で、引数なし関数であることが確認できます。
  4. 関数ボディ (*ast.BlockStmt)
    関数ボディは Body: *ast.BlockStmt で表現され、その中にList: []ast.Stmtとして関数内の文が列挙されています。

    Listには2つのステートメントがあります。

    • 割り当て文 (*ast.AssignStmt) (行43以降)
      Tok: :=から、短変数宣言が行われていることが読み取れます。
      左辺(Lhs)に*ast.Ident{Name: "a"}, 右辺(Rhs)に*ast.BinaryExprが含まれており、a := 1 + 1という構造がそのままツリーになっています。

      *ast.BinaryExprに注目すると、

      • X: *ast.BasicLit {Value: "1"}
      • Op: +
      • Y: *ast.BasicLit {Value: "1"}
        となっており、1 + 1BinaryExprノードで表現されているのがわかります。
        これにより、a1 + 1の計算結果を保持する変数としてスコープ内に登録されます。

      またObjフィールドに着目すると、"a"varとしてast.Objectに登録されており、シンボルテーブル的な側面も持っていることが見て取れます。

    • 関数呼び出し文 (*ast.ExprStmt) (行74以降)
      2つ目の文はfmt.Println(a)を表すExprStmtです。
      X: *ast.CallExprが関数呼び出しを表し、Fun: *ast.SelectorExprによってfmt.Printlnという識別子が階層的に表現されています。

      *ast.SelectorExpr:

      • X: *ast.Ident{Name: "fmt"}
      • Sel: *ast.Ident{Name: "Println"}

      このように、fmt.PrintlnX(レシーバ側)としてfmtというパッケージ識別子、SelとしてPrintlnというメソッドまたは関数名で分解され、ASTがパッケージ識別と、その中の関数参照を明確に表しています。

      Args: []ast.Expr内にはaが含まれ、先ほど定義した変数aがここで使用されていることがわかります。Objフィールドが同じaを指しており、AST内でシンボルを解決できるようになっています。

  5. Unresolved識別子 (Unresolved: []*ast.Ident)
    出力末尾近くに Unresolved: []*ast.Ident (len = 1) { "fmt" } とあります。これはコンパイラ構築中の段階のAST情報で、fmtがまだ解決されていない識別子として記録されていることを示します。
    Goコンパイラはこの後のフェーズ(意味解析)で、fmtimport "fmt"で定義された外部パッケージであると認識し、参照を解決します。このステップまで行けばfmtは未解決リストから外れ、完全な意味解析が完了します。

実際、インタプリタ風にソースコードを実行する評価器を実装してみましたが、その場合にも、上記の様に AST をデータ構造の浅いところから1つ1つ評価していき、動作を実行していくことで Go の簡単なプログラムであれば動作を再現できることが分かってきました。

様々なAST

ここからは、様々なGoの構文におけるASTに登場する要素を見ていきながら、実際機械に処理させる方法を考えてみましょう。

if文のAST

package main

import "fmt"

func sample() {
	a := 1
	if a == 1 {
		a = 2
	} else {
		a = 3
	}
	fmt.Println(a)
}

*ast.IfStmtの該当箇所のみ記載します。
Condに渡された不等式を評価し、true なら Body の処理を、false なら Else の処理を実行する様に動作させると良さそうです。

*ast.IfStmt {
If: ./sample.go:7:2
Cond: *ast.BinaryExpr {
.  X: *ast.Ident {
.  .  NamePos: ./sample.go:7:5
.  .  Name: "a"
.  .  Obj: *(obj @ 48)
.  }
.  OpPos: ./sample.go:7:7
.  Op: ==
.  Y: *ast.BasicLit {
.  .  ValuePos: ./sample.go:7:10
.  .  Kind: INT
.  .  Value: "1"
.  }
}
Body: *ast.BlockStmt {
.  Lbrace: ./sample.go:7:12
.  List: []ast.Stmt (len = 1) {
.  .  0: *ast.AssignStmt {
.  .  .  Lhs: []ast.Expr (len = 1) {
.  .  .  .  0: *ast.Ident {
.  .  .  .  .  NamePos: ./sample.go:8:3
.  .  .  .  .  Name: "a"
.  .  .  .  .  Obj: *(obj @ 48)
.  .  .  .  }
.  .  .  }
.  .  .  TokPos: ./sample.go:8:5
.  .  .  Tok: =
.  .  .  Rhs: []ast.Expr (len = 1) {
.  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  ValuePos: ./sample.go:8:7
.  .  .  .  .  Kind: INT
.  .  .  .  .  Value: "2"
.  .  .  .  }
.  .  .  }
.  .  }
.  }
.  Rbrace: ./sample.go:9:2
}
Else: *ast.BlockStmt {
.  Lbrace: ./sample.go:9:9
.  List: []ast.Stmt (len = 1) {
.  .  0: *ast.AssignStmt {
.  .  .  Lhs: []ast.Expr (len = 1) {
.  .  .  .  0: *ast.Ident {
.  .  .  .  .  NamePos: ./sample.go:10:3
.  .  .  .  .  Name: "a"
.  .  .  .  .  Obj: *(obj @ 48)
.  .  .  .  }
.  .  .  }
.  .  .  TokPos: ./sample.go:10:5
.  .  .  Tok: =
.  .  .  Rhs: []ast.Expr (len = 1) {
.  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  ValuePos: ./sample.go:10:7
.  .  .  .  .  Kind: INT
.  .  .  .  .  Value: "3"
.  .  .  .  }
.  .  .  }
.  .  }
.  }
.  Rbrace: ./sample.go:11:2
}

switch文のAST

package main

import "fmt"

func sample() {
	a := 1
	switch a {
	case 1:
		a = 10
	case 2:
		a = 20
	default:
		a = 0
	}
	fmt.Println(a)
}

*ast.SwitchStmtの該当箇所のみ記載します。
Tagに指定された a の変数を使って、*ast.CaseClauseで case や default 節を評価して該当する処理を実行させれば良さそうです。

*ast.SwitchStmt {
Switch: ./sample.go:7:2
Tag: *ast.Ident {
.  NamePos: ./sample.go:7:9
.  Name: "a"
.  Obj: *(obj @ 48)
}
Body: *ast.BlockStmt {
.  Lbrace: ./sample.go:7:11
.  List: []ast.Stmt (len = 3) {
.  .  0: *ast.CaseClause {
.  .  .  Case: ./sample.go:8:2
.  .  .  List: []ast.Expr (len = 1) {
.  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  ValuePos: ./sample.go:8:7
.  .  .  .  .  Kind: INT
.  .  .  .  .  Value: "1"
.  .  .  .  }
.  .  .  }
.  .  .  Colon: ./sample.go:8:8
.  .  .  Body: []ast.Stmt (len = 1) {
.  .  .  .  0: *ast.AssignStmt {
.  .  .  .  .  Lhs: []ast.Expr (len = 1) {
.  .  .  .  .  .  0: *ast.Ident {
.  .  .  .  .  .  .  NamePos: ./sample.go:9:3
.  .  .  .  .  .  .  Name: "a"
.  .  .  .  .  .  .  Obj: *(obj @ 48)
.  .  .  .  .  .  }
.  .  .  .  .  }
.  .  .  .  .  TokPos: ./sample.go:9:5
.  .  .  .  .  Tok: =
.  .  .  .  .  Rhs: []ast.Expr (len = 1) {
.  .  .  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  .  .  ValuePos: ./sample.go:9:7
.  .  .  .  .  .  .  Kind: INT
.  .  .  .  .  .  .  Value: "10"
.  .  .  .  .  .  }
.  .  .  .  .  }
.  .  .  .  }
.  .  .  }
.  .  }
.  .  1: *ast.CaseClause {
.  .  .  Case: ./sample.go:10:2
.  .  .  List: []ast.Expr (len = 1) {
.  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  ValuePos: ./sample.go:10:7
.  .  .  .  .  Kind: INT
.  .  .  .  .  Value: "2"
.  .  .  .  }
.  .  .  }
.  .  .  Colon: ./sample.go:10:8
.  .  .  Body: []ast.Stmt (len = 1) {
.  .  .  .  0: *ast.AssignStmt {
.  .  .  .  .  Lhs: []ast.Expr (len = 1) {
.  .  .  .  .  .  0: *ast.Ident {
.  .  .  .  .  .  .  NamePos: ./sample.go:11:3
.  .  .  .  .  .  .  Name: "a"
.  .  .  .  .  .  .  Obj: *(obj @ 48)
.  .  .  .  .  .  }
.  .  .  .  .  }
.  .  .  .  .  TokPos: ./sample.go:11:5
.  .  .  .  .  Tok: =
.  .  .  .  .  Rhs: []ast.Expr (len = 1) {
.  .  .  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  .  .  ValuePos: ./sample.go:11:7
.  .  .  .  .  .  .  Kind: INT
.  .  .  .  .  .  .  Value: "20"
.  .  .  .  .  .  }
.  .  .  .  .  }
.  .  .  .  }
.  .  .  }
.  .  }
.  .  2: *ast.CaseClause {
.  .  .  Case: ./sample.go:12:2
.  .  .  Colon: ./sample.go:12:9
.  .  .  Body: []ast.Stmt (len = 1) {
.  .  .  .  0: *ast.AssignStmt {
.  .  .  .  .  Lhs: []ast.Expr (len = 1) {
.  .  .  .  .  .  0: *ast.Ident {
.  .  .  .  .  .  .  NamePos: ./sample.go:13:3
.  .  .  .  .  .  .  Name: "a"
.  .  .  .  .  .  .  Obj: *(obj @ 48)
.  .  .  .  .  .  }
.  .  .  .  .  }
.  .  .  .  .  TokPos: ./sample.go:13:5
.  .  .  .  .  Tok: =
.  .  .  .  .  Rhs: []ast.Expr (len = 1) {
.  .  .  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  .  .  ValuePos: ./sample.go:13:7
.  .  .  .  .  .  .  Kind: INT
.  .  .  .  .  .  .  Value: "0"
.  .  .  .  .  .  }
.  .  .  .  .  }
.  .  .  .  }
.  .  .  }
.  .  }
.  }
.  Rbrace: ./sample.go:14:2
}

for文のAST

package main

import "fmt"

func sample() {
	a := 1
	for i := 0; i < 10; i++ {
		a++
	}
	fmt.Println(a)
}

*ast.ForStmtの該当箇所のみ記載します。
Init でインクリメントの変数を初期化し、Cond が true なら Body の処理を実行します。Body の処理が終わると Post のインクリメント処理を実施し、以降Cond評価が falseになるまで "Cond 評価" -> "Body 実施" -> "Post 実施" を繰り返せば良さそうです。

*ast.ForStmt {
For: ./sample.go:7:2
Init: *ast.AssignStmt {
.  Lhs: []ast.Expr (len = 1) {
.  .  0: *ast.Ident {
.  .  .  NamePos: ./sample.go:7:6
.  .  .  Name: "i"
.  .  .  Obj: *ast.Object {
.  .  .  .  Kind: var
.  .  .  .  Name: "i"
.  .  .  .  Decl: *(obj @ 67)
.  .  .  }
.  .  }
.  }
.  TokPos: ./sample.go:7:8
.  Tok: :=
.  Rhs: []ast.Expr (len = 1) {
.  .  0: *ast.BasicLit {
.  .  .  ValuePos: ./sample.go:7:11
.  .  .  Kind: INT
.  .  .  Value: "0"
.  .  }
.  }
}
Cond: *ast.BinaryExpr {
.  X: *ast.Ident {
.  .  NamePos: ./sample.go:7:14
.  .  Name: "i"
.  .  Obj: *(obj @ 72)
.  }
.  OpPos: ./sample.go:7:16
.  Op: <
.  Y: *ast.BasicLit {
.  .  ValuePos: ./sample.go:7:18
.  .  Kind: INT
.  .  Value: "10"
.  }
}
Post: *ast.IncDecStmt {
.  X: *ast.Ident {
.  .  NamePos: ./sample.go:7:22
.  .  Name: "i"
.  .  Obj: *(obj @ 72)
.  }
.  TokPos: ./sample.go:7:23
.  Tok: ++
}
Body: *ast.BlockStmt {
.  Lbrace: ./sample.go:7:26
.  List: []ast.Stmt (len = 1) {
.  .  0: *ast.IncDecStmt {
.  .  .  X: *ast.Ident {
.  .  .  .  NamePos: ./sample.go:8:3
.  .  .  .  Name: "a"
.  .  .  .  Obj: *(obj @ 48)
.  .  .  }
.  .  .  TokPos: ./sample.go:8:4
.  .  .  Tok: ++
.  .  }
.  }
.  Rbrace: ./sample.go:9:2
}

GolangのASTを覗いてみる――簡単お試し編

ここまで読んで、自分も手元で見てみたい!と思われた方におすすめの簡易的な手法として、以下のサイトを紹介します。

Go AST ViewerというオンラインのASTビューワーであり、任意のコード片をペーストすれば即座にASTツリーが可視化されるため、手軽に内部構造を学べます。

今後の展望:ASTから広がる世界

ASTを理解できれば、インタプリタ・コンパイラだけでなく、コード解析ツールや静的解析器、コード自動変換ツール(codemod)の作成に挑戦できます。

また、他の言語処理系(Python, Rust, C++など)の構文解析にもASTの概念は共通すると考えられるので、横展開してみるのも良さそうです。ASTは言語を跨いだ共通言語のようなもので、言語設計者やツール開発者の視点を得るきっかけにもなります。

まとめ

こうした「ASTを通して言語内部を理解する」経験は、普段なかなか意識しないコンパイラやインタプリタの世界を身近なものにしてくれるはずです。ぜひ一度、自分の書いたコードをASTに落とし込み、その構造を覗いてみてはいかがでしょうか。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?