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

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

このコミットは、Goコンパイラ(cmd/gc)におけるメソッド名のフォーマットに関するバグ修正です。具体的には、メソッド式(method expression)の出力形式が、特にポインタレシーバを持つメソッドの場合に、エクスポート時に正しく表示されない問題(Issue 2877)を解決します。

変更されたファイルは以下の通りです。

  • src/cmd/gc/fmt.c: Goコンパイラのフォーマット処理を行うC言語のソースファイル。メソッド名の特殊なフォーマットケースが追加されました。
  • test/fixedbugs/bug407.dir/one.go: Issue 2877を再現するためのGoのテストファイル。メソッド式を含む型定義とメソッドが記述されています。
  • test/fixedbugs/bug407.dir/two.go: one.goで定義された関数を使用し、インライン化された形式が型チェックされることを確認するためのテストファイル。
  • test/fixedbugs/bug407.go: テストの実行スクリプト。one.gotwo.goをコンパイルしてテストを実行します。

コミット

commit 0b9f0908610bc1d3938a0cb6d33dbfb4c1e9c954
Author: Luuk van Dijk <lvd@golang.org>
Date:   Mon Feb 6 16:38:59 2012 +0100

    cmd/gc: another special (%hhS) case for method names.
    
    Fixes #2877
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5637047

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

https://github.com/golang/go/commit/0b9f0908610bc1d3938a0cb6d33dbfb4c1e9c954

元コミット内容

cmd/gc: another special (%hhS) case for method names.

Fixes #2877

R=rsc
CC=golang-dev
https://golang.org/cl/5637047

変更の背景

このコミットは、Goコンパイラ(cmd/gc)がメソッド式を文字列として出力する際のバグ、具体的にはGo Issue 2877を修正するために行われました。

Go言語では、メソッドはレシーバ(receiver)と呼ばれる特別な引数を持つ関数です。メソッドは型に関連付けられ、その型の値またはポインタに対して呼び出すことができます。Goには「メソッド式(method expression)」という概念があり、これは特定のレシーバインスタンスにバインドされていないメソッドを関数値として参照するものです。例えば、T.Method(*T).Methodのように記述します。

問題は、コンパイラがこれらのメソッド式、特にポインタレシーバを持つメソッド式をエクスポート(外部から参照可能にする)する際に、その名前のフォーマットが期待通りにならないことでした。具体的には、pkg.(*T).methodと表示されるべきところが、pkg.T.methodのようにポインタの*が欠落してしまうケースがあったようです。これは、コンパイラ内部でのノード表現と、それを外部に表示する際の文字列変換ロジックの不一致に起因していました。

この不正確なフォーマットは、デバッグ情報、リフレクション、またはコンパイラが生成する他のメタデータにおいて問題を引き起こす可能性がありました。そのため、コンパイラがメソッド名を正しく、かつ一貫性のある形式で出力するように修正する必要がありました。

前提知識の解説

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラの一つであり、Goのソースコードを機械語に変換する役割を担っています。Goのツールチェインの核心部分であり、型チェック、最適化、コード生成など、コンパイルの主要なフェーズを実行します。このコミットで変更されたfmt.cは、コンパイラが内部的なデータ構造(AST: 抽象構文木)を文字列としてフォーマットする際に使用される部分です。これは、エラーメッセージの生成、デバッグ情報の出力、またはコンパイラが生成する他のテキスト出力に影響します。

メソッドとレシーバ

Goのメソッドは、特定の型に関連付けられた関数です。メソッドはレシーバ引数を持ち、これはメソッドが呼び出される対象のインスタンスを示します。レシーバには「値レシーバ」と「ポインタレシーバ」の2種類があります。

  • 値レシーバ (func (t T) Method(...)): メソッドが呼び出されると、レシーバの型の値がコピーされてメソッドに渡されます。メソッド内でレシーバの値を変更しても、元の値には影響しません。
  • ポインタレシーバ (func (t *T) Method(...)): メソッドが呼び出されると、レシーバの型のポインタがメソッドに渡されます。メソッド内でレシーバの値を変更すると、元の値も変更されます。

メソッド式 (Method Expressions)

Goでは、メソッドを関数値として扱うことができます。これをメソッド式と呼びます。 例えば、type T struct{}; func (t T) M() {}という定義がある場合、T.Mfunc(T)型の関数値となり、(*T).Mfunc(*T)型の関数値となります。 これらのメソッド式は、レシーバを最初の引数として取る通常の関数のように振る舞います。

コンパイラ内部のノード表現 (AST)

コンパイラはソースコードを解析し、その構造を抽象構文木(AST)として内部的に表現します。ASTは、プログラムの各要素(変数、関数、型、式など)をノードとして表現したツリー構造です。 このコミットでは、ONAMEOTYPEといったノードの種類が言及されています。

  • ONAME: 名前(変数名、関数名など)を表すノード。
  • OTYPE: 型を表すノード。

メソッド式の場合、コンパイラはこれを特定のノード構造として表現します。例えば、(*T).fooのようなメソッド式は、ONAMEノードが左の子にOTYPEノード(*Tを表す)を、右の子にONAMEノード(fooを表す)を持つような構造として内部的に扱われることがあります。

フォーマット指定子 (%T, %hhS)

fmt.cのようなコンパイラのフォーマット関数では、C言語のprintfに似たフォーマット指定子が使用されます。

  • %T: 型(Type)をフォーマットするための指定子。
  • %S: シンボル(Symbol)をフォーマットするための指定子。
  • %hhS: シンボルを特殊な形式でフォーマットするための指定子。このコミットでは、メソッド名がパッケージ名なしで表示されるようにするために使用されています。

技術的詳細

このバグは、Goコンパイラがメソッド式、特にポインタレシーバを持つメソッド式を文字列としてフォーマットする際に発生していました。コンパイラ内部では、(*T).methodのようなメソッド式は、ONAMEという種類のノードとして表現されます。このONAMEノードは、さらに左の子にレシーバの型(*T)を表すOTYPEノードを、右の子にメソッド名(method)を表すONAMEノードを持つという特殊な構造をしていました。

問題は、コンパイラがこのONAMEノードをエクスポート(fmtmode == FExp)する際に、ポインタレシーバの*が正しく出力されない場合があったことです。例えば、pkg.(*T).methodと表示されるべきところが、pkg.T.methodのように表示されてしまうことがありました。これは、コンパイラのフォーマットロジックが、この特定のノード構造とエクスポートのコンテキストを十分に考慮していなかったためです。

fmt.cexprfmt関数は、様々な種類の式ノードをフォーマットする役割を担っています。この関数内で、ONAMEノードを処理するcase ONAME:ブロックに、新しい特殊な処理が追加されました。

追加されたロジックは以下の条件をチェックします。

  1. fmtmode == FExp: 現在のフォーマットモードがエクスポート用であること。
  2. n->left && n->left->op == OTYPE: ノードnの左の子が存在し、それがOTYPE(型)ノードであること。
  3. n->right && n->right->op == ONAME: ノードnの右の子が存在し、それがONAME(名前)ノードであること。

これらの条件がすべて満たされる場合、それは(*T).methodT.methodのようなメソッド式を表すONAMEノードであると判断されます。

さらに、レシーバの型がポインタ型であるかどうかをisptr[n->left->type->etype]で確認します。

  • もしポインタ型であれば、fmtprint(f, "(%T).%hhS", n->left->type, n->right->sym)を使って(型).メソッド名の形式で出力します。ここで%Tはレシーバの型(例: *pkg.T)を、%hhSはメソッドのシンボル名(例: foo)をパッケージ名なしで出力します。
  • ポインタ型でなければ、fmtprint(f, "%T.%hhS", n->left->type, n->right->sym)を使って型.メソッド名の形式で出力します。

この修正により、コンパイラはメソッド式をエクスポートする際に、レシーバの型(ポインタの有無を含む)とメソッド名を正確に表現できるようになりました。

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

src/cmd/gc/fmt.cexprfmt関数内のcase ONAME:ブロックに以下のコードが追加されました。

diff --git a/src/cmd/gc/fmt.c b/src/cmd/gc/fmt.c
index 35d33bce87..5437dac1db 100644
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1097,6 +1097,16 @@ exprfmt(Fmt *f, Node *n, int prec)
 		return fmtprint(f, "%V", &n->val);
 
 	case ONAME:
+		// Special case: explicit name of func (*T) method(...) is turned into pkg.(*T).method,
+		// but for export, this should be rendered as (*pkg.T).meth.
+		// These nodes have the special property that they are names with a left OTYPE and a right ONAME.
+		if(fmtmode == FExp && n->left && n->left->op == OTYPE && n->right && n->right->op == ONAME) {
+			if(isptr[n->left->type->etype])
+				return fmtprint(f, "(%T).%hhS", n->left->type, n->right->sym);
+			else
+				return fmtprint(f, "%T.%hhS", n->left->type, n->right->sym);
+		}
+		//fallthrough
 	case OPACK:
 	case ONONAME:
 		return fmtprint(f, "%S", n->sym);

コアとなるコードの解説

追加されたコードは、Goコンパイラが内部的に表現する抽象構文木(AST)の特定のパターンを認識し、それに応じてメソッド名を適切にフォーマットするためのものです。

  1. case ONAME:: このブロックは、コンパイラが名前(変数名、関数名、メソッド名など)を表すONAMEノードを処理する際に実行されます。
  2. // Special case: ...: コメントは、このコードが(*T) method(...)のような形式のメソッドの明示的な名前(メソッド式)を処理するための特殊なケースであることを説明しています。これらのメソッドは、エクスポート時には(*pkg.T).methのようにレンダリングされるべきですが、以前はpkg.T.methのようにポインタの*が欠落する可能性がありました。
  3. if(fmtmode == FExp && n->left && n->left->op == OTYPE && n->right && n->right->op == ONAME):
    • fmtmode == FExp: 現在のフォーマットモードが「エクスポート(Export)」用であることを確認します。これは、外部に公開される名前のフォーマットに影響します。
    • n->left && n->left->op == OTYPE: 現在のONAMEノード(n)が左の子を持ち、その左の子がOTYPE(型)ノードであることを確認します。これは、メソッド式のレシーバ型部分(例: *TT)に対応します。
    • n->right && n->right->op == ONAME: 現在のONAMEノード(n)が右の子を持ち、その右の子がONAME(名前)ノードであることを確認します。これは、メソッド名部分(例: method)に対応します。
    • これらの条件がすべて真である場合、現在のONAMEノードは、(*T).methodT.methodのようなメソッド式を表していると判断されます。
  4. if(isptr[n->left->type->etype]):
    • n->left->type: 左の子(レシーバ型)の実際の型情報を取得します。
    • n->left->type->etype: その型の基本要素型(element type)を取得します。
    • isptr[...]: その要素型がポインタ型であるかどうかをチェックする配列または関数です。
    • この条件は、レシーバがポインタ型(例: *T)であるかどうかを判断します。
  5. return fmtprint(f, "(%T).%hhS", n->left->type, n->right->sym);:
    • レシーバがポインタ型の場合に実行されます。
    • fmtprintはフォーマットされた文字列を出力する関数です。
    • f: フォーマットの出力先(ファイルポインタのようなもの)。
    • "(%T).%hhS": 出力フォーマット文字列。
      • %T: n->left->type(レシーバの型、例: *pkg.T)をフォーマットします。括弧()で囲むことで、ポインタレシーバであることを明示します。
      • .: ドット区切り文字。
      • %hhS: n->right->sym(メソッドのシンボル名、例: foo)をパッケージ名なしでフォーマットします。
    • return: フォーマット処理が完了したため、関数を終了します。
  6. else return fmtprint(f, "%T.%hhS", n->left->type, n->right->sym);:
    • レシーバが値型の場合(ポインタ型でない場合)に実行されます。
    • "%T.%hhS": 出力フォーマット文字列。
      • %T: n->left->type(レシーバの型、例: pkg.T)をフォーマットします。この場合は括弧は不要です。
      • .: ドット区切り文字。
      • %hhS: n->right->sym(メソッドのシンボル名)をフォーマットします。
    • return: フォーマット処理が完了したため、関数を終了します。
  7. //fallthrough: このコメントは、上記の特殊なケースに該当しない場合、処理が次のcase OPACK:case ONONAME:ブロックにフォールスルーすることを示しています。これにより、通常の名前のフォーマット処理が適用されます。

この修正により、コンパイラはメソッド式をエクスポートする際に、レシーバのポインタの有無を正確に反映した形式で出力できるようになり、Issue 2877で報告された表示の不整合が解消されました。

関連リンク

参考にした情報源リンク