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

[インデックス 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言語では、パッケージレベルの変数は宣言順に初期化され、依存関係がある場合はその依存関係が解決されてから初期化されます。もし、ABに依存し、BAに依存するような循環参照がある場合、これは初期化ループと呼ばれ、コンパイルエラーとなります。これは、変数が未初期化の状態で使用されることを防ぐための重要な仕組みです。
    • 例:
      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情報をコピーして、そのn2cgen_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_callmethnを書き換えた後でその参照を使用すると、予期しない、または不整合な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()というパッケージレベル変数の初期化が、メソッド呼び出しを介して自身を参照する初期化ループを引き起こすことを示しています。

  1. commandLineの初期化でNewFlagSet()が呼ばれる。
  2. NewFlagSet()内でf.setErrorHandling(true)が呼ばれる。
  3. setErrorHandling内でf.failf("DIE")が呼ばれる。
  4. failf内でf.usage()が呼ばれる。
  5. usage内でif f == commandLineという比較が行われる。ここでcommandLineが参照される。

この一連の呼び出しによって、commandLineの初期化がcommandLine自身に依存するという循環が発生します。修正前のコンパイラは、cgen_callmethによるASTの「破壊」のためにこの循環を正しく検出できませんでしたが、修正後は// ERROR "loop"コメントが示すように、コンパイル時に「loop」エラーとして検出されるようになります。これにより、バグが修正されたことが確認できます。

関連リンク

参考にした情報源リンク

  • Go言語の初期化順序に関する公式ドキュメントやブログ記事
  • Goコンパイラのソースコード(src/cmd/gcディレクトリ)
  • 抽象構文木(AST)に関する一般的なコンパイラ理論の資料
  • Go言語のIssueトラッカーとコードレビューシステム(Gerrit/CL)