Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10207] ファイルの概要

このコミットは、Goコンパイラのフロントエンドであるgcにおける、主に抽象構文木(AST)のノードの出力(プリンティング)に関するいくつかの小さな修正と、型チェックフェーズでの改善、そしてエスケープ解析のテストケースの修正を含んでいます。具体的には、ASTノードの表示方法の調整、特定の演算子の優先順位の重複定義の解消、そして型チェック時に挿入されるアドレス演算子(OADDR)の扱いに関する修正が行われています。

コミット

commit 29a5ae657fcbd3c673d8c206f4afaa0948538d6b
Author: Luuk van Dijk <lvd@golang.org>
Date:   Wed Nov 2 15:36:33 2011 +0100

    gc: small fixes for printing.
    
    mark OADDR inserted by typecheck as implicit
    OCOPY takes ->left and ->right, not ->list
    OMAKE*'s can all have arguments
    precedence for OIND was initalized twice
    
    fixes #2414
    
    R=rsc, dave
    CC=golang-dev
    https://golang.org/cl/5319065

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/29a5ae657fcbd3c673d8c206f4afaa0948538d6b

元コミット内容

このコミットは、Goコンパイラのgc部分における、プリンティングに関するいくつかの小さな修正を目的としています。具体的には以下の点が挙げられています。

  • typecheckによって挿入されるOADDRノードをimplicit(暗黙的)としてマークする。
  • OCOPYノードが、その引数として->listではなく->left->rightを使用するように修正する。
  • OMAKE*系のノード(OMAKESLICE, OMAKEMAP, OMAKECHANなど)がすべて引数を持つことができるようにする。
  • OIND(間接参照)演算子の優先順位が二重に初期化されていた問題を修正する。

これらの修正は、GoのIssue #2414を解決するものです。

変更の背景

このコミットは、Goコンパイラの内部表現(AST)の正確なプリンティングと、それに伴うコンパイラの挙動の整合性を改善するために行われました。

  1. ASTの正確な表現: コンパイラが内部で扱うASTノードの構造と、それをデバッグやログ出力のために文字列として表現する際の整合性が重要です。特に、fmt.cはASTノードを人間が読める形式で出力するためのコードを含んでおり、ここでの不正確さはコンパイラのデバッグを困難にする可能性があります。
  2. typecheckフェーズの挙動の明確化: typecheckフェーズは、Goのソースコードを型付けし、ASTを変換する重要な段階です。このフェーズで暗黙的に挿入されるノード(例: OADDR)がある場合、それが明示的にマークされることで、コンパイラの挙動がより透過的になります。
  3. 演算子の優先順位の修正: コンパイラがコードを正しく解釈するためには、演算子の優先順位が正確に定義されている必要があります。OINDの優先順位が二重に初期化されていたことは、潜在的なバグや混乱の原因となり得ました。
  4. OMAKE*ノードの引数処理の統一: make組み込み関数は、スライス、マップ、チャネルの作成に使用されます。これらの内部表現であるOMAKE*ノードが、引数の有無にかかわらず一貫した方法で処理されることは、コンパイラのロジックを簡素化し、堅牢性を高めます。
  5. エスケープ解析のテストの修正: コンパイラの変更に伴い、既存のエスケープ解析のテストケースが、新しいコンパイラの挙動に合わせて期待されるエラーメッセージを更新する必要がありました。これは、コンパイラの出力がより正確になったことを示唆しています。

これらの変更は、Goコンパイラの安定性とデバッグ可能性を向上させるための、継続的な改善の一環として行われました。

前提知識の解説

このコミットを理解するためには、Goコンパイラの基本的な構造と、いくつかのコンパイラ理論の概念を知っておく必要があります。

Goコンパイラ (gc) の概要

Goコンパイラ(特にgc、Go 1.5以前の公式コンパイラ)は、複数のフェーズに分かれて動作します。

  • 字句解析 (Lexing): ソースコードをトークンに分割します。
  • 構文解析 (Parsing): トークンから抽象構文木(AST)を構築します。
  • 型チェック (Type Checking): ASTの各ノードの型を決定し、型の一貫性を検証します。このフェーズで、ASTに新しいノードが挿入されたり、既存のノードが変換されたりすることがあります。
  • エスケープ解析 (Escape Analysis): 変数がヒープに割り当てられるべきか(エスケープする)、スタックに割り当てられるべきか(エスケープしない)を決定します。これにより、ガベージコレクションの負荷を軽減します。
  • 最適化 (Optimization): コードのパフォーマンスを向上させるための変換を行います。
  • コード生成 (Code Generation): 最終的な機械語コードを生成します。

抽象構文木 (AST) と Node 構造体

コンパイラは、ソースコードを解析してASTと呼ばれるツリー構造を構築します。ASTはプログラムの構造を抽象的に表現したもので、各ノードは演算子、変数、関数呼び出しなどを表します。

Goコンパイラの内部では、これらのASTノードは通常、Nodeという構造体で表現されます。Node構造体には、ノードの種類を示すOp(オペレーションコード)、子ノードへのポインタ(left, right, listなど)、型情報、値などが含まれます。

  • Op (オペレーションコード): OADD(加算)、OINDEX(配列インデックス)、OADDR(アドレス取得)、OCOPY(コピー)、OMAKESLICE(スライス作成)など、ノードが表す操作の種類を示す列挙型です。
  • left, right: 二項演算子や関数呼び出しの引数など、通常2つの子ノードを持つ場合に用いられます。
  • list: 複数の子ノードを持つ場合(例: 関数呼び出しの引数リスト、複合リテラルの要素リスト)に用いられることがあります。

演算子の優先順位

プログラミング言語では、演算子には優先順位があります。例えば、a + b * ca + (b * c)と解釈され、*+よりも優先されます。コンパイラは、この優先順位情報を使用してASTを正しく構築し、コードを評価します。

opprecのような配列は、各演算子(Opコード)に対応する優先順位の値を格納するために使用されます。

fmt.cの役割

src/cmd/gc/fmt.cファイルは、Goコンパイラのデバッグや診断出力のために、ASTノードを人間が読める形式でフォーマットする関数(exprfmtなど)を含んでいます。これは、コンパイラの内部状態を可視化するために非常に重要です。

エスケープ解析

エスケープ解析は、変数がプログラムの実行中にどこにメモリを割り当てられるべきかを決定するコンパイラの最適化手法です。

  • スタック割り当て: 関数が終了すると自動的に解放される一時的なメモリ領域。高速で、ガベージコレクションの対象外です。
  • ヒープ割り当て: プログラムの実行中に動的に確保され、ガベージコレクタによって管理されるメモリ領域。スタックよりも低速で、ガベージコレクションのオーバーヘッドがあります。

変数が関数のスコープを「エスケープ」して、その関数が終了した後も参照され続ける可能性がある場合、その変数はヒープに割り当てられる必要があります。エスケープ解析は、このようなケースを特定し、適切なメモリ割り当てを決定します。

技術的詳細

このコミットで行われた各修正について、より深く掘り下げて解説します。

mark OADDR inserted by typecheck as implicit

  • OADDRとは: OADDRは、Goの&演算子(アドレス取得演算子)に対応するASTノードです。例えば、&xという式は、xのアドレスを取得する操作を表し、ASTではOADDRノードとして表現されます。
  • typecheckによる挿入: Goコンパイラのtypecheckフェーズでは、特定の状況下で暗黙的にOADDRノードがASTに挿入されることがあります。例えば、配列のスライス操作において、基になる配列が値型である場合、その配列のアドレスが暗黙的に取得されることがあります。
  • implicitフラグ: このコミットでは、typecheckによって挿入されたOADDRノードにimplicitというフラグを立てるようになりました。このフラグは、そのノードがソースコードに明示的に書かれたものではなく、コンパイラが内部的に生成したもの(暗黙的なもの)であることを示します。これにより、デバッグ時やASTの表示時に、コンパイラの挙動がより明確になります。

OCOPY takes ->left and ->right, not ->list

  • OCOPYとは: OCOPYは、Goの組み込み関数copyに対応するASTノードです。copy(dst, src)のように、スライスや配列の要素をコピーする操作を表します。
  • Node構造体の利用: 以前は、OCOPYノードの引数(dstsrc)がn->listというフィールドを通じてアクセスされていた可能性があります。しかし、copy関数は常に2つの引数を取るため、より一般的な二項演算子のようにn->leftn->rightを使用する方が、ASTの構造として自然で一貫性があります。
  • 変更の意図: この変更は、OCOPYノードの内部表現をより正確に、かつ他の二項演算子ノードと一貫性のある形に修正することを目的としています。これにより、fmt.cのようなプリンティングコードが、OCOPYノードの引数をより直感的に処理できるようになります。

OMAKE*'s can all have arguments

  • OMAKE*とは: OMAKESLICEOMAKEMAPOMAKECHANは、それぞれmake組み込み関数によってスライス、マップ、チャネルを作成する操作に対応するASTノードです。
  • 引数の多様性: make関数は、作成する型によって引数の数が異なります。
    • make([]T, len, cap): スライスの場合、長さと容量の引数を持つことができます。
    • make(map[K]V, cap): マップの場合、初期容量の引数を持つことができます。
    • make(chan T, cap): チャネルの場合、バッファ容量の引数を持つことができます。
  • 変更の意図: この修正は、OMAKE*系のすべてのノードが、その種類に応じて適切な引数を持つことができるように、プリンティングロジックを一般化することを目的としています。以前は、OMAKESLICEのみが引数を持つケースが特別扱いされていた可能性がありますが、この変更により、すべてのOMAKE*ノードが統一された方法で引数を処理できるようになりました。特に、n->list->nextを使って引数を取得するロジックが導入され、make関数の引数リストをより柔軟に処理できるようになっています。

precedence for OIND was initalized twice

  • OINDとは: OINDは、Goの*演算子(間接参照演算子、ポインタのデリファレンス)に対応するASTノードです。例えば、*pという式は、ポインタpが指す値を取得する操作を表します。
  • opprec配列: opprecは、各演算子(Opコード)の優先順位を定義する配列です。コンパイラは、この配列を参照して、複雑な式を正しく解析します。
  • 二重初期化の問題: このコミット以前は、OINDの優先順位がopprec配列内で2回初期化されていました。これは、コードの冗長性だけでなく、将来的に優先順位を変更する際に一方の定義だけが更新され、もう一方が忘れられるといった潜在的なバグの原因となり得ます。
  • 修正の意図: この修正は、OINDの優先順位の定義をopprec配列から削除することで、二重初期化の問題を解消し、コードのクリーンアップと保守性の向上を図っています。OINDの優先順位は、他の場所で適切に定義されているか、あるいはデフォルトの優先順位が適用されるようになっています。

test/escape2.goの修正

このファイルは、Goのエスケープ解析のテストケースを含んでいます。コミットメッセージの変更点を見ると、エスケープ解析のエラーメッセージがより正確になるように修正されています。

  • return b.i[:] // ERROR "&b.i escapes to heap" から return b.i[:] // ERROR "b.i escapes to heap"
  • b.ii = b.i[0:4] // ERROR "&b.i escapes to heap" から b.ii = b.i[0:4] // ERROR "b.i escapes to heap"
  • buf = b.i[0:] // ERROR "&b.i escapes to heap" から buf = b.i[0:] // ERROR "b.i escapes to heap"

これらの変更は、&b.ib.iのアドレス)がエスケープするというメッセージから、より直接的にb.i(スライス自体)がヒープにエスケープするというメッセージに変わっています。これは、コンパイラのエスケープ解析がより洗練され、エスケープの原因をより正確に特定できるようになったことを示唆しています。

コアとなるコードの変更箇所

src/cmd/gc/fmt.c

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -921,7 +921,6 @@ static int opprec[] = {
 
 	[OINDEXMAP] = 8,
 	[OINDEX] = 8,
-	[OIND] = 8,
 	[ODOTINTER] = 8,
 	[ODOTMETH] = 8,
 	[ODOTPTR] = 8,
@@ -1146,6 +1145,7 @@ exprfmt(Fmt *f, Node *n, int prec)
 		exprfmt(f, n->left, nprec);
 		return fmtprint(f, "[%N]", n->right);
 
+	case OCOPY:
 	case OCOMPLEX:
 		return fmtprint(f, "%#O(%N, %N)", n->op, n->left, n->right);
 
@@ -1167,7 +1167,6 @@ exprfmt(Fmt *f, Node *n, int prec)
 	case OCAP:
 	case OCLOSE:
 	case OLEN:
-	case OCOPY:
 	case OMAKE:
 	case ONEW:
 	case OPANIC:
@@ -1188,13 +1187,11 @@ exprfmt(Fmt *f, Node *n, int prec)
 			return fmtprint(f, "(%,H...)", n->list);
 		return fmtprint(f, "(%,H)", n->list);
 
-\tcase OMAKESLICE:
-\t\tif(count(n->list) > 2)
-\t\t\treturn fmtprint(f, "make(%T, %N, %N)", n->type, n->left, n->right);   // count list, but print l/r?
-\t\treturn fmtprint(f, "make(%T, %N)", n->type, n->left);
-\n \tcase OMAKEMAP:
 \tcase OMAKECHAN:
+\tcase OMAKESLICE:
+\t\tif(n->list->next)
+\t\t\treturn fmtprint(f, "make(%T, %,H)", n->type, n->list->next);
 \t\treturn fmtprint(f, "make(%T)", n->type);
 
 \tcase OADD:

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -745,6 +745,7 @@ reswitch:
 		defaultlit(&n->right->right, T);
 		if(isfixedarray(n->left->type)) {
 			n->left = nod(OADDR, n->left, N);
+\t\t\tn->left->implicit = 1;
 			typecheck(&n->left, top);
 		}
 		if(n->right->left != N) {

test/escape2.go

--- a/test/escape2.go
+++ b/test/escape2.go
@@ -148,7 +148,7 @@ func (b *Bar2) NoLeak() int { // ERROR "b does not escape"
 }
 
 func (b *Bar2) Leak() []int { // ERROR "leaking param: b"
-\treturn b.i[:] // ERROR "&b.i escapes to heap"
+\treturn b.i[:]  // ERROR "b.i escapes to heap"
 }
 
 func (b *Bar2) AlsoNoLeak() []int { // ERROR "b does not escape"
@@ -156,12 +156,12 @@ func (b *Bar2) AlsoNoLeak() []int { // ERROR "b does not escape"
 }
 
 func (b *Bar2) LeakSelf() { // ERROR "leaking param: b"
-\tb.ii = b.i[0:4] // ERROR "&b.i escapes to heap"
+\tb.ii = b.i[0:4]  // ERROR "b.i escapes to heap"
 }
 
 func (b *Bar2) LeakSelf2() { // ERROR "leaking param: b"
 	var buf []int
-\tbuf = b.i[0:] // ERROR "&b.i escapes to heap"
+\tbuf = b.i[0:]  // ERROR "b.i escapes to heap"
 	b.ii = buf
 }
 

コアとなるコードの解説

src/cmd/gc/fmt.cの変更点

  1. OINDの優先順位の削除: static int opprec[]から[OIND] = 8,の行が削除されました。これは、OINDの優先順位が二重に初期化されていた問題を修正するためです。これにより、opprec配列の定義がよりクリーンになりました。

  2. OCOPYのプリンティングロジックの変更: case OCOPY:case OCOMPLEX:の前に移動し、exprfmt(f, n->left, n->right)のようにn->leftn->rightを使って引数をフォーマットするように変更されました。以前はOCAP, OCLOSE, OLENなどと同じグループにあり、n->listを処理するようなロジックが適用されていた可能性があります。この変更により、OCOPYの引数処理がより正確に反映されるようになりました。

  3. OMAKE*のプリンティングロジックの統一: OMAKESLICEの特別な処理が削除され、OMAKEMAP, OMAKECHANと同じグループにまとめられました。そして、if(n->list->next)という条件が追加され、引数がある場合はmake(%T, %,H)の形式で、引数がない場合はmake(%T)の形式で出力するように統一されました。これは、make関数の引数リストをn->listを通じて一般的に処理できるようにするための改善です。

src/cmd/gc/typecheck.cの変更点

  1. OADDRノードへのimplicitフラグの設定: if(isfixedarray(n->left->type))ブロック内で、n->left = nod(OADDR, n->left, N);によってOADDRノードが挿入された直後に、n->left->implicit = 1;という行が追加されました。これにより、型チェックフェーズで暗黙的に生成されたアドレス取得ノードが、明示的に「暗黙的」としてマークされるようになりました。これは、コンパイラの内部挙動の透明性を高めるための重要な変更です。

test/escape2.goの変更点

このファイルでは、エスケープ解析のテストケースにおける期待されるエラーメッセージが修正されました。具体的には、&b.i escapes to heapというメッセージがb.i escapes to heapに変更されています。これは、エスケープ解析が、ポインタのアドレスではなく、スライス(またはその基になる配列)自体がヒープにエスケープするという、より直接的な原因を報告するようになったことを示しています。これは、コンパイラのエスケープ解析の精度が向上したことを示唆しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード (src/cmd/gcディレクトリ内のファイル)
  • コンパイラ設計に関する一般的な知識