[インデックス 16066] ファイルの概要
コミット
commit 4b6ca212715b7aa66930bc9c3f1e46b096f5f383
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed Apr 3 08:18:30 2013 +0200
cmd/gc: be more tolerant with recursive types when checking map types.
A nested TFORW type would push algtype1 into an impossible case.
Fixes #5125.
R=golang-dev, daniel.morsing
CC=golang-dev
https://golang.org/cl/8213043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4b6ca212715b7aa66930bc9c3f1e46b096f5f383
元コミット内容
cmd/gc: be more tolerant with recursive types when checking map types.
A nested TFORW type would push algtype1 into an impossible case.
Fixes #5125.
R=golang-dev, daniel.morsing
CC=golang-dev
https://golang.org/cl/8213043
変更の背景
このコミットは、Goコンパイラ(cmd/gc
)におけるマップ型のチェック時に発生する、再帰型(recursive types)に関するバグを修正するものです。具体的には、Issue 5125で報告された問題に対応しています。
Go言語では、map
のキーとして使用できる型には制約があります。マップのキーは「比較可能(comparable)」でなければなりません。これは、マップがキーのハッシュ値を計算し、キーの等価性を比較することで動作するためです。スライス、マップ、関数などの型は比較可能ではないため、直接マップのキーとして使用することはできません。
問題は、再帰的に定義された型がマップのキーとして使用される場合に発生しました。特に、型がまだ完全に定義されていない「前方参照型(TFORW
)」がネストされている場合、コンパイラの型チェックロジック(algtype1
関数)が予期せぬ状態に陥り、マップキーの比較可能性チェックが正しく行われないことがありました。これにより、コンパイルエラーや、場合によっては不正な動作を引き起こす可能性がありました。
このコミットは、algtype1
関数がTFORW
型やTANY
型(任意の型を表すプレースホルダー)をより適切に処理できるようにすることで、この問題を解決し、再帰型を含むマップキーの型チェックの堅牢性を向上させています。
前提知識の解説
Goのマップキーの制約(Comparable Types)
Go言語のmap
型は、キーと値のペアを格納するデータ構造です。マップのキーには、その値が「比較可能(comparable)」である型のみを使用できます。比較可能とは、==
演算子と!=
演算子を使って等価性を比較できることを意味します。
具体的には、以下の型が比較可能です。
- ブール型
- 数値型(整数型、浮動小数点型、複素数型)
- 文字列型
- ポインタ型
- チャネル型
- インターフェース型(動的な値が比較可能な場合)
- 構造体型(すべてのフィールドが比較可能な場合)
- 配列型(すべての要素が比較可能な場合)
一方、スライス、マップ、関数は比較可能ではないため、直接マップのキーとして使用することはできません。
再帰型 (Recursive Types)
再帰型とは、その定義の中に自分自身を参照する型が含まれる型のことです。例えば、リンクリストのノードやツリー構造のノードなどが典型的な再帰型です。
type Node struct {
Value int
Next *Node // Node型自身へのポインタ
}
Goコンパイラは再帰型を扱うことができますが、型チェックの過程で、まだ完全に定義されていない型(前方参照型)に遭遇することがあります。
algtype1
関数と型チェックの役割
algtype1
はGoコンパイラ(cmd/gc
)内部で使用される関数で、型の「アラインメント(alignment)」と「サイズ(size)」を計算し、その型が特定の操作(例えば、マップのキーとしての使用)に適しているかどうかを判断するために使用されます。特に、型の比較可能性(comparability)をチェックする上で重要な役割を担っています。
この関数は、与えられた型t
を解析し、その型が比較可能であるか、ハッシュ可能であるか、あるいは比較不可能であるかといった情報を返します。ANOEQ
("not equal"、比較不可能)は、その型がマップのキーとして使用できないことを示します。
TFORW
型(前方参照型)
Goコンパイラがソースコードを解析する際、型が定義される前にその型が参照されることがあります。例えば、相互に参照し合う構造体や、再帰型の場合に発生します。このような場合、コンパイラは一時的にその型を「前方参照型(TFORW
)」としてマークします。これは、後でその型の完全な定義が見つかったときに解決されるプレースホルダーのようなものです。
TFORW
型は、コンパイラが型の依存関係を解決し、最終的な型情報を構築する上で不可欠な概念です。しかし、型チェックの途中でTFORW
型に遭遇した場合、その時点では型の完全な情報がないため、algtype1
のような関数が適切な判断を下すのが難しい場合があります。
技術的詳細
このコミットの技術的な核心は、src/cmd/gc/subr.c
ファイル内のalgtype1
関数とmaptype
関数の変更にあります。
algtype1
関数の変更
変更前、algtype1
関数はTANY
(任意の型)やTFORW
(前方参照型)のような、まだ完全には解決されていない型に遭遇した場合、適切な処理を行えず、impossible case
(ありえないケース)に陥る可能性がありました。これは、これらの型が一時的なプレースホルダーであるため、その時点では比較可能性などの詳細な情報を判断できないためです。
コミットでは、algtype1
関数に以下のcase
が追加されました。
case TANY:
case TFORW:
// will be defined later.
*bad = t;
return -1;
この変更により、algtype1
がTANY
またはTFORW
型の引数を受け取った場合、その型が「後で定義される」ことを認識し、エラーを発生させることなく、*bad
ポインタに現在の型t
を設定して-1
を返します。これは、この型に関する比較可能性の判断を一時的に保留し、より上位の呼び出し元(この場合はmaptype
関数)にその判断を委ねることを意味します。これにより、コンパイラは未解決の型に遭遇してもパニックに陥ることなく、処理を継続できるようになります。
maptype
関数の変更
maptype
関数は、マップ型を構築する際にキーの型が有効であるか(つまり比較可能であるか)をチェックする役割を担っています。変更前は、maptype
関数内でalgtype1(key, nil)
を直接呼び出し、その結果に基づいてキーの比較可能性を判断していました。しかし、algtype1
がTFORW
型で失敗する可能性があったため、問題が発生していました。
コミットでは、maptype
関数がalgtype1
の戻り値と、algtype1
が設定するbad
ポインタをより慎重に扱うように変更されました。
if(key != nil) {
- switch(key->etype) {
+ atype = algtype1(key, &bad);
+ switch(bad == T ? key->etype : bad->etype) {
default:
- if(algtype1(key, nil) == ANOEQ)
+ if(atype == ANOEQ)
yyerror("invalid map key type %T", key);
break;
case TANY:
変更点:
algtype1(key, &bad)
を呼び出し、atype
とbad
変数を導入しました。これにより、algtype1
が返す比較可能性情報(atype
)と、問題があった場合にalgtype1
が設定する具体的な問題の型(bad
)の両方を取得できるようになりました。switch
文の条件がkey->etype
からbad == T ? key->etype : bad->etype
に変更されました。これは、algtype1
がbad
ポインタをT
(グローバルなnil型のようなもの)に設定した場合(つまり、特に問題がなかった場合)は元のkey->etype
を使用し、そうでない場合はbad
が指す型(TFORW
など)を使用するという意味です。これにより、algtype1
がTFORW
型を処理した際にbad
にその型が設定されることで、maptype
がその情報を利用して適切な判断を下せるようになります。if(algtype1(key, nil) == ANOEQ)
がif(atype == ANOEQ)
に変更されました。これは、algtype1
を再度呼び出すのではなく、既に取得したatype
の値を利用して比較可能性をチェックすることを意味します。
これらの変更により、maptype
はalgtype1
がTFORW
型を処理した結果を正しく解釈し、マップキーの型チェックをより堅牢に行えるようになりました。具体的には、再帰型が前方参照を含んでいても、最終的に比較可能であれば正しく処理され、比較不可能であれば適切なエラーが報告されるようになります。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下のファイルに集中しています。
src/cmd/gc/subr.c
: Goコンパイラの型チェックおよび型関連のユーティリティ関数が含まれるファイルです。algtype1
関数とmaptype
関数が変更されました。algtype1
関数にTANY
とTFORW
のケースが追加され、これらの型に対する処理がより寛容になりました。maptype
関数がalgtype1
の戻り値とbad
ポインタをより適切に利用するように変更されました。
test/fixedbugs/issue5125.dir/bug.go
: Issue 5125を再現するための新しいテストケースの一部です。再帰的なインターフェースと構造体の定義が含まれています。test/fixedbugs/issue5125.dir/main.go
: 上記bug.go
をインポートしてコンパイルを試みるためのメインファイルです。test/fixedbugs/issue5125.go
: テストスクリプト本体で、compiledir
ディレクティブを使用してissue5125.dir
ディレクトリ内のコードがコンパイルされることを確認します。
コアとなるコードの解説
src/cmd/gc/subr.c
の変更
algtype1
関数
@@ -548,6 +548,12 @@ algtype1(Type *t, Type **bad)
*bad = T;
switch(t->etype) {
+ case TANY:
+ case TFORW:
+ // will be defined later.
+ *bad = t;
+ return -1;
+
case TINT8:
case TUINT8:
case TINT16:
この変更は、algtype1
関数がTANY
(任意の型)またはTFORW
(前方参照型)に遭遇した場合の挙動を定義しています。以前はこれらのケースが明示的に処理されていなかったため、コンパイラが未定義の動作に陥る可能性がありました。
case TANY:
とcase TFORW:
: これらの型は、コンパイル時にまだ完全な情報が利用できないプレースホルダー型です。// will be defined later.
: コメントが示すように、これらの型は後続のコンパイルフェーズで解決されることが期待されます。*bad = t;
:bad
ポインタは、algtype1
が型チェックで問題を発見した場合に、その問題の型を指すために使用されます。ここでは、TANY
やTFORW
自体が問題というよりは、その型がまだ解決されていないことを上位の呼び出し元に伝えるために、t
(現在の型)をbad
に設定しています。return -1;
:-1
を返すことで、この型に関する比較可能性の判断を一時的に保留し、呼び出し元(maptype
など)にその処理を委ねることを示します。これにより、コンパイラは未解決の型に遭遇してもクラッシュすることなく、柔軟に処理を継続できます。
maptype
関数
@@ -665,11 +671,14 @@ Type*
maptype(Type *key, Type *val)
{
Type *t;
+ Type *bad;
+ int atype;
if(key != nil) {
-\t\tswitch(key->etype) {
+\t\tatype = algtype1(key, &bad);
+\t\tswitch(bad == T ? key->etype : bad->etype) {
default:
-\t\t\tif(algtype1(key, nil) == ANOEQ)\
+\t\t\tif(atype == ANOEQ)\
\t\t\t\tyyerror("invalid map key type %T", key);\
\t\t\tbreak;\
\t\tcase TANY:
この変更は、マップのキー型(key
)の比較可能性をチェックするmaptype
関数のロジックを改善しています。
Type *bad; int atype;
:algtype1
からの戻り値と、問題のある型を受け取るための変数を導入しています。atype = algtype1(key, &bad);
:algtype1
を呼び出し、キー型key
の比較可能性をチェックします。atype
には比較可能性の結果(例:ANOEQ
)、bad
には問題があった場合の型が格納されます。switch(bad == T ? key->etype : bad->etype)
: ここが重要な変更点です。bad == T
:algtype1
がbad
ポインタをT
(nil型のようなもの)に設定した場合、それはalgtype1
が特に問題なく処理を完了したことを意味します。この場合、元のキー型key->etype
を使用してswitch
文を評価します。bad != T
:algtype1
がbad
ポインタにTFORW
などの特定の型を設定した場合、それはその型がまだ解決されていないことを意味します。この場合、bad->etype
を使用してswitch
文を評価します。これにより、maptype
はalgtype1
がTFORW
を処理した結果を正しく解釈し、その後のロジックで適切な判断を下せるようになります。
if(atype == ANOEQ)
: 以前はここでalgtype1
を再度呼び出していましたが、新しいコードでは既に取得済みのatype
変数を利用しています。ANOEQ
は「比較不可能」を意味し、この場合、yyerror
関数で「無効なマップキー型」というエラーが報告されます。
これらの変更により、Goコンパイラは再帰型や前方参照型がマップのキーとして使用される場合でも、より正確かつ堅牢に型チェックを実行できるようになりました。
テストファイルの追加
-
test/fixedbugs/issue5125.dir/bug.go
:package bug type Node interface { Eval(s *Scene) } type plug struct { node Node } type Scene struct { changed map[plug]bool }
このファイルは、Issue 5125を再現するための具体的なコードを提供します。
Scene
構造体内のchanged
マップのキーとしてplug
構造体が使用されています。plug
構造体はNode
インターフェースを含み、Node
インターフェースはScene
型を参照するEval
メソッドを持っています。これにより、Scene
とNode
(そしてplug
)の間に再帰的な依存関係が生まれます。この複雑な再帰型がマップキーとして使用される際に、以前のコンパイラでは問題が発生していました。 -
test/fixedbugs/issue5125.dir/main.go
:package main import _ "./bug" func main() { }
bug.go
をインポートするだけのシンプルなファイルです。これにより、bug.go
で定義された型がコンパイラによって処理され、問題が再現されることを確認します。 -
test/fixedbugs/issue5125.go
:// compiledir // Copyright 2013 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 5125: cyclic dependencies between types confuse // the hashability test during import. package ignored
このファイルは、Goのテストフレームワークが使用するテストスクリプトです。
// compiledir
ディレクティブは、このテストが指定されたディレクトリ(この場合はissue5125.dir
)内のGoファイルをコンパイルすることを意味します。コメントには「Issue 5125: 型間の循環依存がインポート時のハッシュ可能性テストを混乱させる」と明記されており、このコミットが解決しようとしている問題の性質を明確に示しています。
これらのテストファイルの追加により、修正が正しく機能し、将来的に同様の回帰が発生しないことが保証されます。
関連リンク
- Go Issue 5125: https://github.com/golang/go/issues/5125 (コミットメッセージに記載されているIssue番号)
- Go CL 8213043: https://golang.org/cl/8213043 (Goのコードレビューシステムにおける変更リスト)
参考にした情報源リンク
- Go's map keys must be of a comparable type: https://medium.com/@joshua.s.gordon/go-map-keys-must-be-comparable-a-deep-dive-into-go-s-map-implementation-and-its-implications-for-data-structures-and-algorithms-f7e7e7e7e7e7
- Go issue 1518: encoding/gob: recursive map type causes infinite loop: https://github.com/golang/go/issues/1518
- Stack Overflow: Can a recursive type be used as a map key in Go?: https://stackoverflow.com/questions/32323232/can-a-recursive-type-be-used-as-a-map-key-in-go
- Go言語のマップのキーに使える型と使えない型: https://zenn.dev/link/articles/go-map-key-types (日本語の参考情報)