[インデックス 11296] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)におけるバグ修正に関するものです。具体的には、マップリテラルにおいて、誤った型のキーが暗黙的に正しい型に変換される場合に、コンパイラがキーの重複チェックを適切に行わない問題を修正しています。これにより、本来であればコンパイルエラーとなるべきコードが誤ってコンパイルされてしまう可能性がありました。
コミット
commit 46e7cb57c951724630a722c55cda684889a7123b
Author: Jeff R. Allen <jra@nella.org>
Date: Fri Jan 20 13:34:38 2012 -0500
gc: do not try to add a key with incorrect type to a hash
Fixes #2623.
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5533043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/46e7cb57c951724630a722c55cda684889a7123b
元コミット内容
このコミットは、Go言語のIssue 2623で報告されたバグを修正するものです。元の問題は、マップリテラルを初期化する際に、キーの型がマップの宣言されたキーの型と一致しない場合でも、コンパイラが暗黙的な型変換(例えば、int
からstring
への変換)を試み、その結果、キーの重複チェックが正しく機能しないというものでした。
具体的には、map[string]int
のようなマップに対して、"abc": 1, 1: 2
のように、1
というint
型の値をstring
型のキーとして指定した場合、Goコンパイラは通常であれば型不一致のエラーを報告します。しかし、このバグが存在するバージョンでは、1
がstring
型に変換可能であると誤って判断され、"1"
という文字列として扱われてしまい、その後のキー重複チェックがスキップされるか、あるいは誤ったコンテキストで実行されていました。これにより、本来コンパイル時に検出されるべき型エラーが報告されず、予期せぬ動作を引き起こす可能性がありました。
変更の背景
Go言語のコンパイラは、コードの型安全性を保証するために厳密な型チェックを行います。マップリテラルも例外ではなく、キーと値の型はマップの宣言と一致している必要があります。しかし、Go言語には暗黙的な型変換(例えば、数値リテラルが適切な型に変換されるなど)のルールも存在します。
Issue 2623は、この型変換のロジックとマップリテラルのキー重複チェックのロジックが相互に作用する際に発生する問題でした。コンパイラがマップリテラルのキーを処理する際、まずキーの式を評価し、必要に応じてマップのキー型に変換します。この変換処理(assignconv
)が成功すると、その結果のノードに対してkeydup
関数が呼び出され、マップリテラル内でキーが重複していないかどうかがチェックされます。
問題は、assignconv
が型変換を行った結果、元のノードのオペレーションがOCONV
(型変換オペレーション)になる場合、keydup
がそのOCONV
ノードを適切に処理できない、またはOCONV
ノードの背後にある元の値の型を正しく認識できないことにありました。これにより、型変換が行われたキーに対しては、重複チェックがスキップされるか、誤った比較が行われ、結果として型エラーが報告されないという状況が生じていました。
このバグは、コンパイラが開発者の意図しない型変換を許容し、かつその後の重要なチェックを怠るという、型安全性を損なう深刻な問題であったため、早急な修正が必要とされました。
前提知識の解説
Go言語の型システムと型変換 (Type Conversion)
Go言語は静的型付け言語であり、変数は特定の型を持ちます。異なる型の値を直接代入することは、通常は許されません。しかし、Goには特定の条件下で暗黙的な型変換が行われる場合があります。例えば、数値リテラルは、その値が収まる範囲であれば、int
、float64
などの異なる数値型に自動的に変換されることがあります。また、明示的な型変換(例: string(1)
)も可能です。
このコミットの文脈では、マップリテラルのキーがマップの宣言されたキー型と異なる場合に、コンパイラが暗黙的な変換を試みる挙動が問題となります。
Go言語のマップ (map) の動作とキーの型
Goのマップは、キーと値のペアを格納するハッシュテーブルです。マップのキーは、比較可能(comparable)な型である必要があります(例: 整数、文字列、ポインタ、構造体、配列など)。マップリテラルを初期化する際には、map[KeyType]ValueType{key1: value1, key2: value2, ...}
のように記述します。この際、各keyN
の型はKeyType
と互換性がある必要があります。
Goコンパイラのgc
(Go Compiler) の役割、特にtypecheck.c
が担当する型チェックフェーズ
gc
はGo言語の公式コンパイラです。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成など、複数のフェーズがあります。
src/cmd/gc/typecheck.c
は、コンパイラの型チェックフェーズを担当するC言語のソースファイルです。このファイルには、GoプログラムのAST(抽象構文木)を走査し、各ノード(変数、関数呼び出し、リテラルなど)の型がGo言語の仕様に準拠しているかを確認するロジックが含まれています。型チェックは、プログラムの正しさを保証し、実行時エラーを未然に防ぐ上で非常に重要なステップです。
OCONV
オペレーションの意味
OCONV
は、Goコンパイラの内部表現(ASTノードのオペレーションコード)の一つで、「型変換(Conversion)」を表します。コンパイラが、ある型の値を別の型に変換する必要があると判断した場合、その変換操作を表すOCONV
ノードがASTに挿入されます。例えば、int
型の1
をstring
型に変換するような場合、内部的にはOCONV
ノードが生成されます。
keydup
関数の役割
keydup
関数は、マップリテラル内でキーが重複していないかをチェックするために使用されるコンパイラ内部の関数です。Go言語のマップリテラルでは、同じキーを複数回指定することはできません。もし重複するキーが指定された場合、コンパイルエラーとなるべきです。keydup
は、ハッシュテーブル(hash
とnhash
)を使用して、既に処理されたキーを追跡し、新しいキーが既存のキーと重複していないかを確認します。
技術的詳細
このバグは、src/cmd/gc/typecheck.c
内のtypecheckcomplit
関数で発生していました。この関数は、複合リテラル(マップリテラルや構造体リテラルなど)の型チェックを行います。
マップリテラルのキーを処理する際、以下のステップが実行されます。
typecheck(&l->left, Erv);
:キーの式自体の型チェックを行います。defaultlit(&l->left, t->down);
:デフォルトリテラル変換を適用します(例:1
がint
型に変換されるなど)。l->left = assignconv(l->left, t->down, "map key");
:キーの式をマップの宣言されたキー型(t->down
)に変換します。この関数は、必要に応じて暗黙的な型変換を行い、変換後のASTノードを返します。もし変換が行われた場合、返されるノードのオペレーションはOCONV
となる可能性があります。keydup(l->left, hash, nhash);
:変換後のキーノード(l->left
)を使用して、マップリテラル内でキーが重複していないかをチェックします。
問題は、ステップ3でassignconv
によって型変換が行われ、l->left
がOCONV
ノードになった場合、ステップ4のkeydup
がそのOCONV
ノードを正しく扱えず、キーの重複チェックがスキップされてしまうことにありました。例えば、map[string]int{"abc":1, 1:2}
というコードでは、1
というint
リテラルがstring
型に変換される際にOCONV
ノードが生成されます。このOCONV
ノードがkeydup
に渡されると、keydup
は"1"
という文字列としてのキーを正しく認識できず、重複チェックが機能しなかったのです。
修正は、keydup
を呼び出す前に、l->left
がOCONV
ノードであるかどうかをチェックする条件を追加することでした。もしl->left
がOCONV
ノードであれば、それは既に型変換が行われたことを意味し、keydup
を呼び出す必要がない、あるいはkeydup
が正しく処理できないケースであると判断されます。この修正により、OCONV
ノードがkeydup
に渡されることを防ぎ、コンパイラが型不一致のキーを正しくエラーとして報告できるようになりました。
コアとなるコードの変更箇所
src/cmd/gc/typecheck.c
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2130,7 +2130,8 @@ typecheckcomplit(Node **np)
typecheck(&l->left, Erv);
defaultlit(&l->left, t->down);
l->left = assignconv(l->left, t->down, "map key");
- keydup(l->left, hash, nhash);
+ if (l->left->op != OCONV)
+ keydup(l->left, hash, nhash);
r = l->right;
pushtype(r, t->type);
test/fixedbugs/bug397.go
(新規追加)
// errchk $G -e $D/$F.go
// Copyright 2011 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.
package main
// Issue 2623
var m = map[string]int {
"abc":1,
1:2, // ERROR "cannot use 1.*as type string in map key"
}
コアとなるコードの解説
src/cmd/gc/typecheck.c
の変更
変更はtypecheckcomplit
関数内で行われています。この関数は、マップリテラルなどの複合リテラルの要素を処理するループの一部です。
元のコードでは、マップキーの型チェックと変換を行った後、無条件にkeydup
関数を呼び出してキーの重複をチェックしていました。
l->left = assignconv(l->left, t->down, "map key");
keydup(l->left, hash, nhash);
修正後のコードでは、keydup
の呼び出しに条件が追加されています。
l->left = assignconv(l->left, t->down, "map key");
if (l->left->op != OCONV)
keydup(l->left, hash, nhash);
このif (l->left->op != OCONV)
という条件が重要です。
l->left
は、マップリテラルの現在のキーを表すASTノードです。l->left->op
は、そのノードのオペレーションコードです。OCONV
は、型変換が行われたことを示すオペレーションコードです。
この条件は、「もし現在のキーノードが型変換(OCONV
)の結果ではない場合のみ、keydup
を呼び出す」という意味になります。
なぜこの条件が必要なのか?
assignconv
関数は、キーの型がマップの宣言されたキー型と異なる場合に、そのキーを適切な型に変換しようとします。例えば、map[string]int
に対して1:2
というエントリがあった場合、assignconv
は1
(int
型リテラル)をstring
型に変換しようとします。この変換が成功すると、l->left
はOCONV
オペレーションを持つノードに変わります。
元のバグは、OCONV
ノードがkeydup
に渡されたときに、keydup
がそのノードを正しく処理できず、結果としてキーの重複チェックが機能しなかったことにありました。OCONV
ノードは、その内部に変換元の値と変換先の型情報を持つ複雑な構造であるため、keydup
が直接そのノードから「キーの値」を抽出してハッシュ計算を行うのが困難だったと考えられます。
この修正により、assignconv
によって型変換が行われた(つまりl->left->op
がOCONV
である)キーに対しては、keydup
が呼び出されなくなります。これにより、コンパイラは、型変換によって曖昧になったキーの重複チェックを試みる代わりに、より早い段階で型不一致のエラーを報告するようになります。test/fixedbugs/bug397.go
の例では、1:2
というエントリはmap[string]int
に対して型不一致であるため、assignconv
の段階でエラーが検出され、keydup
が呼び出される前にコンパイルエラーとなることが期待されます。
test/fixedbugs/bug397.go
の解説
このファイルは、修正が正しく機能することを確認するための新しいテストケースです。
// errchk $G -e $D/$F.go
この行は、Goコンパイラ($G
)を実行し、エラー(-e
)をチェックすることを指示しています。期待される動作は、指定されたファイル($D/$F.go
)のコンパイルがエラーになることです。
var m = map[string]int {
"abc":1,
1:2, // ERROR "cannot use 1.*as type string in map key"
}
このコードスニペットがテストの核心です。
map[string]int
というマップが宣言されています。キーはstring
型、値はint
型です。- 最初のエントリ
"abc":1
は正しい型です。 - 二番目のエントリ
1:2
が問題の箇所です。ここではint
型の1
がキーとして指定されていますが、マップのキーはstring
型であるべきです。
このテストの目的は、この1:2
という行が「cannot use 1.*as type string in map key
」というエラーメッセージを伴ってコンパイルエラーになることを確認することです。修正前は、このエラーが適切に報告されず、1
が暗黙的に"1"
に変換されてしまう可能性がありました。修正後は、assignconv
の段階で型不一致が検出され、keydup
が呼び出される前にコンパイルエラーが報告されるようになります。
関連リンク
- Go Issue 2623: https://github.com/golang/go/issues/2623
- Gerrit Change 5533043: https://golang.org/cl/5533043
参考にした情報源リンク
- Go言語の公式ドキュメント (マップ、型変換に関するセクション)
- Goコンパイラのソースコード (
src/cmd/gc/typecheck.c
の周辺コード) - Go言語のIssueトラッカー (Issue 2623の議論)
- Go言語のGerritコードレビューシステム (CL 5533043のレビューコメント)
- Go言語のコンパイラ設計に関する一般的な情報源