[インデックス 14041] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるメソッド呼び出しのコード生成ロジックの修正に関するものです。具体的には、抽象構文木(AST)の不適切な変更が原因で発生していた、初期化ループの検出に関するバグを修正しています。
コミット
commit 0b2ca9e62f778adb95c31e57e120ef1cbfc42b25
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Sun Oct 7 00:52:40 2012 +0200
cmd/gc: avoid clobbering the AST in cgen_callmeth.
It confused the detection of init loops when involving
method calls.
Fixes #3890.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6620067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0b2ca9e62f778adb95c31e57e120ef1cbfc42b25
元コミット内容
cmd/gc: avoid clobbering the AST in cgen_callmeth.
It confused the detection of init loops when involving
method calls.
Fixes #3890.
変更の背景
このコミットは、Go言語のコンパイラ(cmd/gc
)が、メソッド呼び出しを含む初期化ループを正しく検出できないというバグ(Issue 3890)を修正するために行われました。
Go言語では、パッケージレベルの変数の初期化順序は厳密に定義されており、初期化の依存関係が循環している場合(初期化ループ)はコンパイル時にエラーとして検出されるべきです。しかし、特定の条件下でメソッド呼び出しが初期化式に含まれる場合、コンパイラがASTを不適切に「破壊(clobbering)」してしまうことで、この初期化ループの検出ロジックが混乱し、本来検出されるべきエラーが見過ごされてしまう問題がありました。
具体的には、cgen_callmeth
関数がメソッド呼び出しのASTノードを変換する際に、元のASTノードを直接変更していました。これにより、初期化順序を解析する別のフェーズが、変更されたASTノードを参照した際に、誤った依存関係を認識してしまい、結果として初期化ループを検出できなくなっていたと考えられます。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラの一つで、Goソースコードを機械語に変換します。コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成などが含まれます。 - 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したものです。コンパイラの各フェーズでASTが構築され、変換され、最終的に実行可能なコードが生成されます。コンパイラがコードの意味を理解し、最適化やエラーチェックを行う上で不可欠なデータ構造です。
- 初期化ループ (Initialization Loop): Go言語では、パッケージレベルの変数は宣言順に初期化され、依存関係がある場合はその依存関係が解決されてから初期化されます。もし、
A
がB
に依存し、B
がA
に依存するような循環参照がある場合、これは初期化ループと呼ばれ、コンパイルエラーとなります。これは、変数が未初期化の状態で使用されることを防ぐための重要な仕組みです。- 例:
このようなコードは初期化ループとして検出され、コンパイルエラーになります。var a = b + 1 // a は b に依存 var b = a + 1 // b は a に依存
- 例:
cgen_callmeth
: Goコンパイラのコード生成(cgen
)フェーズにおける関数の一つで、メソッド呼び出し(例:receiver.Method(args)
)を処理します。Goのメソッド呼び出しは、内部的にはレシーバを最初の引数とする通常の関数呼び出しに変換されます(例:Method(receiver, args)
)。この変換処理がcgen_callmeth
で行われます。- ASTの「破壊 (clobbering)」: プログラミングの文脈で「clobbering」とは、意図せずデータやメモリの内容を上書きしたり破壊したりすることを指します。このコミットの文脈では、
cgen_callmeth
関数が、メソッド呼び出しを表すASTノードを変換する際に、そのノード自体を直接変更してしまい、そのノードを参照している他のコンパイラフェーズ(特に初期化ループ検出フェーズ)が古い、または不整合な情報を参照してしまう状態を指します。
技術的詳細
Goコンパイラのcmd/gc
では、ソースコードをASTに変換した後、様々な最適化やチェックを行います。その中には、パッケージレベル変数の初期化順序を決定し、初期化ループを検出するフェーズがあります。
問題は、cgen_callmeth
関数がメソッド呼び出しのASTノードを変換する際に発生していました。元のコードでは、n->op = OCALLFUNC;
や n->left = n->left->right;
のように、引数として渡されたNode *n
(メソッド呼び出しを表すASTノード)のフィールドを直接書き換えていました。
// 変更前のcgen_callmeth (抜粋)
void
cgen_callmeth(Node *n, int proc)
{
Node *l;
// generate a rewrite for method call
// (p.f)(...) goes to (f)(p,...)
l = n->left;
if(l->op != ODOTMETH)
fatal("cgen_callmeth: not dotmethod: %N");
n->op = OCALLFUNC; // ここでnのopを直接変更
n->left = n->left->right; // ここでnのleftを直接変更
n->left->type = l->type;
if(n->left->op == ONAME)
n->left->class = PFUNC;
cgen_call(n, proc); // 変更されたnをcgen_callに渡す
}
この直接的な変更が、初期化ループを検出するsinit.c
内のロジックに影響を与えていました。sinit.c
はASTを走査して初期化の依存関係を構築しますが、cgen_callmeth
がASTノードを「その場で」変更してしまうため、sinit.c
が走査するASTが、cgen_callmeth
が実行される前と後で異なってしまい、初期化ループの検出が混乱していました。特に、メソッド呼び出しが初期化式に含まれる場合にこの問題が顕在化しました。
修正は、cgen_callmeth
内で新しいNode
構造体n2
を導入し、元のn
を直接変更する代わりに、n2
に変換後のAST情報をコピーして、そのn2
をcgen_call
関数に渡すようにしました。これにより、元のASTノードn
は変更されずに残り、初期化ループ検出フェーズが正しいAST構造を参照できるようになります。
また、src/cmd/gc/sinit.c
にも小さな変更が加えられています。これは、型に紐付けられたメソッド式(Type.Method(receiver, ...)
のような形式)の定義がtype->nname
に格納されている場合、その定義も初期化の依存関係として適切に処理されるようにするためのものです。これは、メソッド呼び出しが初期化ループ検出に影響を与えるという根本的な問題に関連しています。
test/fixedbugs/bug459.go
は、このバグを再現するためのテストケースです。var commandLine = NewFlagSet()
という初期化式が、NewFlagSet
内でsetErrorHandling
を呼び出し、それがfailf
を呼び出し、さらにusage
を呼び出し、最終的にcommandLine
自身を参照するという循環を意図的に作り出しています。この循環が、修正前はコンパイラによって検出されず、修正後は「loop」エラーとして正しく検出されることを確認します。
コアとなるコードの変更箇所
src/cmd/gc/gen.c
cgen_callmeth
関数が変更されました。
--- a/src/cmd/gc/gen.c
+++ b/src/cmd/gc/gen.c
@@ -509,22 +509,24 @@ ret:
void
cgen_callmeth(Node *n, int proc)
{
+ Node n2; // 新しいNode構造体を宣言
Node *l;
- // generate a rewrite for method call
+ // generate a rewrite in n2 for the method call
// (p.f)(...) goes to (f)(p,...)
l = n->left;
if(l->op != ODOTMETH)
fatal("cgen_callmeth: not dotmethod: %N");
- n->op = OCALLFUNC; // 元のnを直接変更していた箇所
- n->left = n->left->right; // 元のnを直接変更していた箇所
- n->left->type = l->type;
+ n2 = *n; // 元のnの内容をn2にコピー
+ n2.op = OCALLFUNC; // n2のopを変更
+ n2.left = l->right; // n2のleftを変更
+ n2.left->type = l->type;
- if(n->left->op == ONAME)
- n->left->class = PFUNC;
- cgen_call(n, proc); // 変更されたnを渡していた
+ if(n2.left->op == ONAME)
+ n2.left->class = PFUNC;
+ cgen_call(&n2, proc); // 変更されたn2を渡す
}
/*
src/cmd/gc/sinit.c
init1
関数にコメントと条件が追加されました。
--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -36,7 +36,9 @@ init1(Node *n, NodeList **out)\n init1(n->right, out);\n for(l=n->list; l; l=l->next)\n init1(l->n, out);\n+\n if(n->left && n->type && n->left->op == OTYPE && n->class == PFUNC) {\n+\t\t// Methods called as Type.Method(receiver, ...).\n \t\t// Definitions for method expressions are stored in type->nname.\n \t\tinit1(n->type->nname, out);\n \t}\
test/fixedbugs/bug459.go
新しいテストファイルが追加されました。
// errorcheck
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Issue 3890: missing detection of init loop involving
// method calls in function bodies.
package flag
var commandLine = NewFlagSet() // ERROR "loop"
type FlagSet struct {
}
func (f *FlagSet) failf(format string, a ...interface{}) {
f.usage()
}
func (f *FlagSet) usage() {
if f == commandLine {
panic(3)
}
}
func NewFlagSet() *FlagSet {
f := &FlagSet{}
f.setErrorHandling(true)
return f
}
func (f *FlagSet) setErrorHandling(b bool) {
f.failf("DIE")
}
コアとなるコードの解説
src/cmd/gc/gen.c
の変更
cgen_callmeth
関数は、Goのメソッド呼び出しをコンパイラが処理しやすい形式に変換する役割を担っています。Goでは、receiver.Method(args)
のようなメソッド呼び出しは、内部的にはMethod(receiver, args)
のような通常の関数呼び出しとして扱われます。この変換はASTレベルで行われます。
変更前は、cgen_callmeth
が引数として受け取ったNode *n
(メソッド呼び出しのASTノード)のop
フィールドやleft
フィールドを直接書き換えていました。これは、ASTノードを「その場で」変換するインプレース変換です。
// 変更前: nを直接変更
n->op = OCALLFUNC;
n->left = n->left->right;
このインプレース変換が問題でした。なぜなら、コンパイラの他の部分、特に初期化ループを検出するロジックが、このn
ノードへの参照を保持している可能性があり、cgen_callmeth
がn
を書き換えた後でその参照を使用すると、予期しない、または不整合なAST構造を参照してしまうからです。これにより、初期化の依存関係が正しく解析されず、初期化ループが見過ごされる原因となっていました。
修正では、新しいNode
型のローカル変数n2
を導入し、元のn
の内容をn2
にコピーします。そして、このn2
に対して変換処理(n2.op = OCALLFUNC;
など)を行い、最終的にcgen_call(&n2, proc);
として、変換後のn2
を次の処理に渡します。
// 変更後: n2にコピーして変換
Node n2;
n2 = *n; // nの内容をn2にコピー
n2.op = OCALLFUNC;
n2.left = l->right;
// ...
cgen_call(&n2, proc); // n2を渡す
この変更により、元のn
ノードはcgen_callmeth
によって変更されなくなり、初期化ループ検出フェーズが常に正しいAST構造を参照できるようになりました。これは、コンパイラの異なるフェーズ間でのASTの整合性を保つための重要な修正です。
src/cmd/gc/sinit.c
の変更
sinit.c
は、Goの初期化順序を解析し、初期化ループを検出する役割を担っています。init1
関数は、ASTノードを再帰的に走査し、初期化の依存関係をout
リストに収集します。
追加されたコードは、メソッド式(Type.Method(receiver, ...)
のような形式)の定義がtype->nname
に格納されている場合に、その定義もinit1
で処理されるようにするためのものです。
if(n->left && n->type && n->left->op == OTYPE && n->class == PFUNC) {
// Methods called as Type.Method(receiver, ...).
// Definitions for method expressions are stored in type->nname.
init1(n->type->nname, out);
}
これは、cgen_callmeth
の変更と合わせて、メソッド呼び出しが初期化の依存関係にどのように影響するかをより正確に追跡するために必要だったと考えられます。n->type->nname
は、型に紐付けられたメソッドのシンボル情報や、そのメソッドが関数として表現された場合のASTノードを指すことがあります。この部分を初期化解析の対象に含めることで、メソッド呼び出しを介した初期化ループも適切に検出できるようになります。
test/fixedbugs/bug459.go
の追加
このテストケースは、var commandLine = NewFlagSet()
というパッケージレベル変数の初期化が、メソッド呼び出しを介して自身を参照する初期化ループを引き起こすことを示しています。
commandLine
の初期化でNewFlagSet()
が呼ばれる。NewFlagSet()
内でf.setErrorHandling(true)
が呼ばれる。setErrorHandling
内でf.failf("DIE")
が呼ばれる。failf
内でf.usage()
が呼ばれる。usage
内でif f == commandLine
という比較が行われる。ここでcommandLine
が参照される。
この一連の呼び出しによって、commandLine
の初期化がcommandLine
自身に依存するという循環が発生します。修正前のコンパイラは、cgen_callmeth
によるASTの「破壊」のためにこの循環を正しく検出できませんでしたが、修正後は// ERROR "loop"
コメントが示すように、コンパイル時に「loop」エラーとして検出されるようになります。これにより、バグが修正されたことが確認できます。
関連リンク
- Go Issue 3890: https://github.com/golang/go/issues/3890
- Go CL 6620067: https://golang.org/cl/6620067
参考にした情報源リンク
- Go言語の初期化順序に関する公式ドキュメントやブログ記事
- Goコンパイラのソースコード(
src/cmd/gc
ディレクトリ) - 抽象構文木(AST)に関する一般的なコンパイラ理論の資料
- Go言語のIssueトラッカーとコードレビューシステム(Gerrit/CL)