[インデックス 1624] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)におけるバグ修正を目的としています。具体的には、関数が複数の戻り値を返す際に、その戻り値が別の関数の引数として渡される場合に発生する型チェックの問題を解決します。特に、戻り値の変数名が呼び出し側の期待する引数名と異なる場合に、コンパイラが正しく型を比較できないという問題に対処しています。この修正により、f(g())
のような形式で、g
が複数の戻り値を返し、f
がそれらを引数として受け取るシナリオが正しく機能するようになります。
コミット
commit b0009bef20badeb3716ed94c8291accc75cf769e
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 5 15:22:49 2009 -0800
bug064
make f(g()) work when g returns multiple
args with names different than f expects.
func swap(a, b int) (c, d int) {
return b, a
}
swap(swap(1,2))
R=ken
OCL=24474
CL=24476
---
src/cmd/gc/go.h | 3 +--
src/cmd/gc/subr.c | 19 +++++++++++++++++++
src/cmd/gc/walk.c | 4 ++--
test/{bugs => fixedbugs}/bug064.go | 0
test/golden.out | 6 ------
5 files changed, 22 insertions(+), 10 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b0009bef20badeb3716ed94c8291accc75cf769e
元コミット内容
bug064
make f(g()) work when g returns multiple
args with names different than f expects.
func swap(a, b int) (c, d int) {
return b, a
}
swap(swap(1,2))
R=ken
OCL=24474
CL=24476
変更の背景
このコミットは、Go言語の初期段階で発見されたコンパイラのバグ「bug064」を修正するために行われました。このバグは、関数が複数の戻り値を返す際に、その戻り値が別の関数の引数として渡される場合に顕在化しました。特に問題となったのは、戻り値の型は一致しているものの、戻り値に付けられた変数名(例えば func swap(a, b int) (c, d int)
の c, d
)が、呼び出し側の関数が期待する引数名と異なる場合に、コンパイラが型互換性を正しく判断できないという点でした。
Go言語では、複数の戻り値をサポートしており、これは非常に強力な機能です。しかし、コンパイラがこれらの戻り値の型チェックを適切に行わないと、予期せぬコンパイルエラーや実行時エラーにつながる可能性があります。swap(swap(1,2))
のようなネストされた関数呼び出しで、内部の swap
が返す (int, int)
が外部の swap
の (a, b int)
引数に渡される際に、コンパイラが戻り値の「名前」に依存してしまい、型の一致を見落としていたと考えられます。
この修正は、Go言語の型システムとコンパイラの堅牢性を向上させ、より複雑な関数呼び出しパターンを安全に記述できるようにするために不可欠でした。bug064.go
というテストファイルがこのバグの再現と修正の検証のために作成され、修正後には fixedbugs
ディレクトリに移動され、回帰テストとして機能しています。
前提知識の解説
このコミットの理解には、以下のGo言語およびコンパイラの基本的な概念が役立ちます。
-
Go言語の複数戻り値: Go言語の関数は、複数の値を返すことができます。例えば、
func foo() (int, string)
のように定義され、return 1, "hello"
のように返されます。これらの戻り値は、呼び出し側で複数の変数に同時に代入することができます(例:x, y := foo()
)。 -
関数呼び出しの引数としての複数戻り値: Goでは、ある関数の複数戻り値を、別の関数の引数として直接渡すことができます。例えば、
func f(a, b int)
とfunc g() (int, int)
がある場合、f(g())
のように記述できます。この際、g()
の戻り値の数と型がf
の引数の数と型に一致している必要があります。 -
コンパイラの型チェック: コンパイラは、プログラムが型規則に準拠しているかを検証する役割を担います。これには、関数呼び出しにおける引数と戻り値の型の互換性チェックが含まれます。Go言語のコンパイラ
gc
は、この型チェックを厳密に行います。 -
構造体型と名前: Go言語において、構造体(
struct
)は複合型の一種です。関数の戻り値や引数のリストは、コンパイラの内部では匿名構造体のような形で表現されることがあります。この際、構造体のフィールド名(関数の引数名や戻り値名)は、型の一致性とは別に扱われるべき情報です。 -
src/cmd/gc
: これはGo言語のコンパイラgc
のソースコードがあるディレクトリです。gc
はGo言語で書かれたプログラムを機械語に変換する役割を担っています。このディレクトリ内のファイルは、字句解析、構文解析、型チェック、コード生成など、コンパイルの各段階を処理します。go.h
: コンパイラの内部で使用される共通のヘッダーファイルで、型定義や外部変数宣言などが含まれます。subr.c
: サブルーチンやユーティリティ関数が含まれるファイルで、型システムに関連する関数もここに定義されることがあります。walk.c
: 抽象構文木(AST)を走査し、型チェックや最適化、中間コード生成などを行う「ウォーカー」のロジックが含まれるファイルです。
技術的詳細
このバグの核心は、Goコンパイラ gc
が複数戻り値の型チェックを行う際に、型の「名前」に過度に依存していた点にあります。Go言語の型システムでは、構造体のフィールド名や関数の引数・戻り値名は、型の同一性を判断する際には通常考慮されません。例えば、struct { A int }
と struct { B int }
は、フィールド名が異なっていても、その構造が同じであれば(この場合はどちらも int
型の単一フィールドを持つ)、多くの場合で互換性があると見なされます。
bug064
のケースでは、swap(swap(1,2))
のような呼び出しにおいて、内部の swap
関数が (c, d int)
という名前付きの戻り値を返します。この (c, d int)
という戻り値の型は、外部の swap
関数が期待する (a, b int)
という引数の型と、その構造(どちらも int
型の2つの値)は同じです。しかし、コンパイラ内部の型比較ロジック eqtype
が、この名前の違いを誤って不一致と判断していた可能性があります。
この問題を解決するために、新しい型比較関数 eqtypenoname
が導入されました。この関数は、特に構造体型(Goコンパイラ内部で複数戻り値や引数リストを表すために使用される)を比較する際に、フィールド名(Sym*
)を無視して、その基盤となる型(Type*
)と順序のみを比較するように設計されています。
src/cmd/gc/walk.c
の ascompatte
関数は、代入互換性をチェックする際に使用されます。この関数内で、以前は eqtype
を使用して戻り値の型と期待される引数の型を比較していましたが、これが eqtypenoname
に置き換えられました。これにより、戻り値の名前が異なっていても、その構造と型が一致していれば、正しく互換性があると判断されるようになりました。
この変更は、Go言語の型システムが「構造的型付け(structural typing)」の原則に則っていることをより明確に反映するものです。構造的型付けでは、型の互換性は、その型の構造(フィールドの型と順序、メソッドのシグネチャなど)によって決定され、明示的な名前による宣言的な関係(名目型付け)には依存しません。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
-
src/cmd/gc/go.h
:EXTERN int func;
の行が削除されました。これはこのバグ修正とは直接関係ない、おそらくクリーンアップの一環です。int eqtypenoname(Type*, Type*);
という新しい関数のプロトタイプ宣言が追加されました。
-
src/cmd/gc/subr.c
:eqtypenoname
関数が新しく追加されました。この関数は、eqtype
をラップし、特に構造体型(TSTRUCT
)の比較において、フィールド名(Sym*
)を無視して型のみを比較するように設計されています。
+int +eqtypenoname(Type *t1, Type *t2) +{ + if(t1 == T || t2 == T || t1->etype != TSTRUCT || t2->etype != TSTRUCT) + return eqtype(t1, t2, 0); + + + t1 = t1->type; + t2 = t2->type; + for(;;) { + if(!eqtype(t1, t2, 1)) // d=1 means ignore names + return 0; + if(t1 == T) + return 1; + t1 = t1->down; + t2 = t2->down; + } +}
-
src/cmd/gc/walk.c
:ascompatte
関数内で、型互換性をチェックする部分が変更されました。- 以前は
eqtype(r->type, *nl, 0)
を使用していましたが、これがeqtypenoname(r->type, *nl)
に置き換えられました。これにより、戻り値の型と期待される引数の型の比較において、名前が無視されるようになりました。
- && eqtype(r->type, *nl, 0))\n- return convas(nod(OAS, nodarg(*nl, fp), r)); + && eqtypenoname(r->type, *nl))\n+ return convas(nod(OAS, nodarg(r->type, fp), r));
-
test/bugs/bug064.go
からtest/fixedbugs/bug064.go
への移動:- バグが修正されたことを示すために、テストファイルが
bugs
ディレクトリからfixedbugs
ディレクトリへ移動されました。これは、このテストが回帰テストとして機能し、将来的に同じバグが再発しないことを保証するためのものです。
- バグが修正されたことを示すために、テストファイルが
-
test/golden.out
の変更:test/golden.out
は、テストの期待される出力を含むファイルです。bug064.go
のバグが修正されたため、以前のコンパイルエラーに関する記述が削除されました。
コアとなるコードの解説
このコミットの核心は、eqtypenoname
関数の導入とその ascompatte
関数での利用にあります。
eqtypenoname
関数:
この関数は、Goコンパイラの型システムにおいて、特に構造体型(TSTRUCT
)の比較を行う際に、そのフィールド名(Go言語の引数名や戻り値名に相当)を無視して、純粋にその構造と基盤となる型のみを比較するためのものです。
if(t1 == T || t2 == T || t1->etype != TSTRUCT || t2->etype != TSTRUCT)
: この条件は、比較対象の型t1
またはt2
が無効な型 (T
) であるか、あるいはどちらかが構造体型 (TSTRUCT
) でない場合に、既存のeqtype
関数を呼び出すことを意味します。これは、eqtypenoname
が特に構造体型の名前を無視するロジックに特化しているためです。t1 = t1->type; t2 = t2->type;
: 構造体型の場合、t1->type
とt2->type
は、その構造体の最初のフィールドの型情報へのポインタを指します。Goコンパイラ内部では、構造体のフィールドはリンクリストのように連結されています。for(;;) { ... }
: このループは、構造体のすべてのフィールドを順に比較するために使用されます。if(!eqtype(t1, t2, 1))
: ここが重要なポイントです。eqtype
関数がd=1
という引数(d
は「depth」または「detail」を示す可能性があり、ここでは「名前を無視する」フラグとして機能する)と共に呼び出されています。これにより、個々のフィールドの型を比較する際に、そのフィールド名が異なっていても型が一致していればtrue
を返します。if(t1 == T) return 1;
: これは、すべてのフィールドの比較が完了し、両方の構造体の終端に達したことを意味します。この場合、型は互換性があると判断されます。t1 = t1->down; t2 = t2->down;
: 次のフィールドの型情報に進みます。
ascompatte
関数での利用:
ascompatte
関数は、代入互換性(assignment compatibility)をチェックするコンパイラ内部の関数です。これは、ある式の値が別の変数や引数に代入可能かどうかを判断する際に使用されます。
swap(swap(1,2))
のようなケースでは、内部の swap
の戻り値 (c, d int)
が、外部の swap
の引数 (a, b int)
に代換可能であるかを ascompatte
がチェックします。以前は eqtype(r->type, *nl, 0)
を使用していましたが、0
という引数は名前を考慮して型を比較することを意味していました。そのため、戻り値の名前 c, d
と引数名 a, b
の違いによって、コンパイラが型が不一致であると誤って判断していました。
この修正により、eqtypenoname(r->type, *nl)
が使用されることで、戻り値の型と期待される引数の型の比較において、名前の違いが無視されるようになりました。これにより、Go言語の構造的型付けの原則が正しく適用され、名前が異なっていても構造的に同じ型であれば、互換性があると判断されるようになり、bug064
が解決されました。
関連リンク
bug064.go
テストファイル: このコミットで修正されたバグの回帰テストとして機能します。Go言語のソースコードリポジトリ内でtest/fixedbugs/bug064.go
として見つけることができます。
参考にした情報源リンク
- GitHubコミットページ: https://github.com/golang/go/commit/b0009bef20badeb3716ed94c8291accc75cf769e
- Web検索結果 (Go language bug064):
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGYyXm3GHyD-oPXEUzRT6r1dElG3nZouOVI-JgEAwo2luQvBgLeERQKpWqssUlJhrd5xD6g2Z4x9lLqisyAjII1S5PFGdrQ4CqkCWDVGztZdqverQ1N89eMGAUtLuml0PF_cPEjHmbY-Xu9cX31uKJJaT5yTeNg6WXUwhaDRg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHBCNq55U2fC7yLbZV1eL30BQjYG-OQlkRJF5Yj02qPNKNs_gm4h7ZQHBiXFtmgEpwf5qfu0nKUAsvK1qOL4O1iFA1eEucue1oerE6oEJHa5Q1EvRFdclPUpCkd3d0CjB1utVt-g7SeWIiNNavLDn1BElHUfVt3S2yurO1eqKqEESkbDvxwZtqkC6B_ndWGnUoLz2U_ZjUs5ZYaDyQIWHU3pAD